@instruments/materialgraph 0.12.0 → 0.13.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/browse-types.d.ts +88 -10
- package/by-colour-types.d.ts +17 -5
- package/client-browse.d.ts +15 -0
- package/client-browse.js +19 -0
- package/client.d.ts +6 -9
- package/client.js +9 -9
- package/corpus-scope-types.d.ts +55 -0
- package/corpus-scope-types.js +25 -0
- package/demand-types.d.ts +20 -0
- package/errors.d.ts +29 -0
- package/errors.js +43 -0
- package/exploration-types.d.ts +3 -5
- package/index.d.ts +3 -1
- package/index.js +1 -1
- package/material-query-plan-types.d.ts +154 -0
- package/material-query-plan-types.js +1 -0
- package/operations.d.ts +1 -1
- package/operations.js +1 -0
- package/package.json +1 -1
- package/query-understanding-types.d.ts +38 -0
- package/query-understanding-types.js +1 -0
- package/search-refinement-types.d.ts +26 -3
- package/search-request-types.d.ts +62 -7
- package/types.d.ts +67 -6
package/browse-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ByColourSwatch } from "./by-colour-types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Wire types for the browse lane — GET `/api/v1/search/browse`.
|
|
3
4
|
*
|
|
@@ -12,9 +13,9 @@
|
|
|
12
13
|
* Nothing about the client's surface changed: `index.ts` re-exports these
|
|
13
14
|
* exactly as before.
|
|
14
15
|
*/
|
|
15
|
-
import type {
|
|
16
|
+
import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
16
17
|
import type { Cursor, ProductId, VariantId } from "./identifiers.js";
|
|
17
|
-
import type { CountedTotal, OpenEnum } from "./types.js";
|
|
18
|
+
import type { CountedTotal, OpenEnum, SearchFacets } from "./types.js";
|
|
18
19
|
/** One item from the pure colour-scalar browse. */
|
|
19
20
|
export interface BrowseResultItem {
|
|
20
21
|
/** Opaque — round-trip verbatim. */
|
|
@@ -43,9 +44,20 @@ export interface BrowseResultItem {
|
|
|
43
44
|
* from product imagery, not from the manufacturer.
|
|
44
45
|
*/
|
|
45
46
|
palette?: ByColourSwatch[];
|
|
47
|
+
/**
|
|
48
|
+
* Resolved values for the attribute ids sent in `returnAttributes`, keyed by
|
|
49
|
+
* attribute id. ABSENT entirely when `returnAttributes` was not sent; a key
|
|
50
|
+
* whose value is `null` means this material carries no accepted value for that
|
|
51
|
+
* attribute. Three states, all different.
|
|
52
|
+
*/
|
|
53
|
+
attributes?: Record<string, string | string[] | null>;
|
|
46
54
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The browse orders. `name` (A→Z) is the default, `name_desc` the same index
|
|
57
|
+
* read Z→A, `recent` newest first; the rest are the colour ramps. `/search`
|
|
58
|
+
* takes all eight too — one word, one order, on both lanes.
|
|
59
|
+
*/
|
|
60
|
+
export type BrowseSort = OpenEnum<"name" | "name_desc" | "recent" | "light" | "dark" | "vivid" | "muted" | "colourful">;
|
|
49
61
|
/**
|
|
50
62
|
* The browse feed's total — how many materials the browse DESCRIBES, which is
|
|
51
63
|
* never `results.length`.
|
|
@@ -78,21 +90,43 @@ export interface BrowseResponse {
|
|
|
78
90
|
* to label its own listing.
|
|
79
91
|
*/
|
|
80
92
|
sort: BrowseSort;
|
|
93
|
+
/**
|
|
94
|
+
* The attribute ladder this page was ordered by — present ONLY when `orderBy`
|
|
95
|
+
* was sent, in which case it SUPERSEDED `sort` (still echoed above, and still
|
|
96
|
+
* describing nothing this page was ordered by).
|
|
97
|
+
*/
|
|
98
|
+
orderBy?: {
|
|
99
|
+
attributeId: string;
|
|
100
|
+
direction: OpenEnum<"asc" | "desc">;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Reader prose about how the request was interpreted — today, only that
|
|
104
|
+
* `orderBy` superseded a STATED `sort`. Absent means nothing needed saying.
|
|
105
|
+
*/
|
|
106
|
+
note?: string;
|
|
81
107
|
/**
|
|
82
108
|
* The total, present ONLY when `count: true` was passed. Absent means it was
|
|
83
109
|
* not asked for — never that the set is empty.
|
|
84
110
|
*/
|
|
85
111
|
counts?: BrowseCounts;
|
|
112
|
+
/**
|
|
113
|
+
* Per-facet value counts, present ONLY when `facetCounts` was asked for.
|
|
114
|
+
*
|
|
115
|
+
* Counted over this browse's OWN match set — the same predicate the page and
|
|
116
|
+
* `counts` ran under — so every value is `basis:"exact"` and `poolSize` is
|
|
117
|
+
* ABSENT. That differs from {@link SearchResponse}'s facets, which are
|
|
118
|
+
* normally counted over a ranked candidate pool and are therefore floors:
|
|
119
|
+
* browse retrieves no pool, so there is nothing for a figure to be a floor
|
|
120
|
+
* of. A facet the server could not count in time appears in `absent` with
|
|
121
|
+
* `reason:"timeout"` and the results are unaffected.
|
|
122
|
+
*/
|
|
123
|
+
facets?: SearchFacets;
|
|
86
124
|
/**
|
|
87
125
|
* The corpus this page was drawn from — present ONLY when the request
|
|
88
126
|
* narrowed it. ABSENT means the whole graph: every material MaterialGraph
|
|
89
127
|
* knows, orderable or not.
|
|
90
128
|
*/
|
|
91
|
-
corpusScope?:
|
|
92
|
-
scope: OpenEnum<"all" | "materialBankOrderable">;
|
|
93
|
-
/** What the scope admits, in one sentence, for a human reader. */
|
|
94
|
-
description: string;
|
|
95
|
-
};
|
|
129
|
+
corpusScope?: CorpusScopeReceipt;
|
|
96
130
|
}
|
|
97
131
|
/** Params for the pure colour `search/browse` surface. */
|
|
98
132
|
export interface BrowseParams {
|
|
@@ -136,6 +170,30 @@ export interface BrowseParams {
|
|
|
136
170
|
* brand's fan deck. An unknown brand is a 400 naming it, never an empty page.
|
|
137
171
|
*/
|
|
138
172
|
brands?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Typed attribute filters, in the SAME grammar `search` takes — a term, a
|
|
175
|
+
* term SET (OR within the entry), a presence check or a numeric range, each
|
|
176
|
+
* optionally carrying a `trust` bar. Entries AND together.
|
|
177
|
+
*
|
|
178
|
+
* Each entry is sent as ITS OWN `filters=` parameter, JSON-encoded, which is
|
|
179
|
+
* what the route reads. Pass the objects; the client does the encoding.
|
|
180
|
+
*
|
|
181
|
+
* Applied INSIDE the keyset scan and counted under the same predicate, so a
|
|
182
|
+
* filtered page still fills to `limit`, `cursor` still means another MATCHING
|
|
183
|
+
* row exists, and `counts.variants` describes the FILTERED set. An attribute
|
|
184
|
+
* id outside the registry is a 400 carrying ranked candidates, never an empty
|
|
185
|
+
* page.
|
|
186
|
+
*/
|
|
187
|
+
filters?: readonly Record<string, unknown>[];
|
|
188
|
+
/**
|
|
189
|
+
* Facet ids to count over this browse's match set — `brand`, `category`, or a
|
|
190
|
+
* spec attribute id. Sent as one comma-separated `facetCounts=` parameter.
|
|
191
|
+
*
|
|
192
|
+
* The response's `facets` block comes back `basis:"exact"` with no
|
|
193
|
+
* `poolSize`, because browse counts the whole predicate rather than a ranked
|
|
194
|
+
* pool. Omit it for no `facets` block and no counting cost.
|
|
195
|
+
*/
|
|
196
|
+
facetCounts?: readonly string[];
|
|
139
197
|
/**
|
|
140
198
|
* Which corpus to walk. ABSENT means the whole graph — every material
|
|
141
199
|
* MaterialGraph knows, orderable or not. `materialBankOrderable` restricts to
|
|
@@ -143,5 +201,25 @@ export interface BrowseParams {
|
|
|
143
201
|
* scoped page is still a full page and `cursor` still means another eligible
|
|
144
202
|
* row exists.
|
|
145
203
|
*/
|
|
146
|
-
corpusScope?:
|
|
204
|
+
corpusScope?: CorpusScope;
|
|
205
|
+
/**
|
|
206
|
+
* Sort along ONE attribute's declared value ladder instead of a `sort` word —
|
|
207
|
+
* the same field, ladder and refusals `search` takes. Sent as one JSON-encoded
|
|
208
|
+
* `orderBy=` parameter; pass the object, the client encodes it.
|
|
209
|
+
*
|
|
210
|
+
* SUPERSEDES `sort` when both are given, and the response `note` says so.
|
|
211
|
+
* Ordering is DECLARED, never inferred from a value's spelling: an attribute
|
|
212
|
+
* with no stated ladder is a 400 carrying `orderable_attributes`. Materials
|
|
213
|
+
* off the ladder, or carrying no value, sort LAST in both directions.
|
|
214
|
+
*/
|
|
215
|
+
orderBy?: {
|
|
216
|
+
attributeId: string;
|
|
217
|
+
direction?: OpenEnum<"asc" | "desc">;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Attribute ids whose resolved value rides each result under `attributes`, so
|
|
221
|
+
* a filtered listing can display what it filtered on. Sent as one
|
|
222
|
+
* comma-separated `returnAttributes=` parameter.
|
|
223
|
+
*/
|
|
224
|
+
returnAttributes?: readonly string[];
|
|
147
225
|
}
|
package/by-colour-types.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* budget. Same tolerant-parsing contract as the rest of the mirror — response
|
|
7
7
|
* enums are widened so a new server value still type-checks.
|
|
8
8
|
*/
|
|
9
|
+
import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
9
10
|
import type { VariantId } from "./identifiers.js";
|
|
11
|
+
import type { QueryUnderstanding } from "./query-understanding-types.js";
|
|
10
12
|
import type { OklabTriple, OpenEnum, PaletteSwatch } from "./types.js";
|
|
11
13
|
/** Coarse group + fine type of a colour match's primary product type. */
|
|
12
14
|
export interface MatchCategory {
|
|
@@ -26,6 +28,12 @@ export interface ByColourMatch {
|
|
|
26
28
|
variantId: VariantId;
|
|
27
29
|
brand: string;
|
|
28
30
|
product: string;
|
|
31
|
+
/** Record-route brand identity. */
|
|
32
|
+
brandSlug?: string | null;
|
|
33
|
+
/** Opaque MaterialGraph product-family identity. */
|
|
34
|
+
productId?: string | null;
|
|
35
|
+
/** Material Bank cart/order join key; absent when not on record. */
|
|
36
|
+
materialBankId?: string;
|
|
29
37
|
color: string | null;
|
|
30
38
|
variantName: string | null;
|
|
31
39
|
imageUrl: string | null;
|
|
@@ -147,6 +155,8 @@ export interface ColourLaneReceipt {
|
|
|
147
155
|
/** The `search/colours` response. */
|
|
148
156
|
export interface ByColourResponse {
|
|
149
157
|
query: {
|
|
158
|
+
/** Raw query provenance, when supplied. */
|
|
159
|
+
q?: string;
|
|
150
160
|
colours: string[];
|
|
151
161
|
categories?: string[];
|
|
152
162
|
/**
|
|
@@ -186,14 +196,14 @@ export interface ByColourResponse {
|
|
|
186
196
|
* counts describe the SCOPED corpus too, because the scope was applied inside
|
|
187
197
|
* retrieval rather than over the page it produced.
|
|
188
198
|
*/
|
|
189
|
-
corpusScope?:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
description: string;
|
|
193
|
-
};
|
|
199
|
+
corpusScope?: CorpusScopeReceipt;
|
|
200
|
+
/** Shared standard-search query-understanding receipt. */
|
|
201
|
+
understanding?: QueryUnderstanding;
|
|
194
202
|
}
|
|
195
203
|
/** Params for `search/colours`. */
|
|
196
204
|
export interface ByColourParams {
|
|
205
|
+
/** Raw material-domain query provenance for shared query understanding. */
|
|
206
|
+
q?: string;
|
|
197
207
|
/** A single hex (with or without `#`). */
|
|
198
208
|
hex?: string;
|
|
199
209
|
/** A palette — repeated as one `colour` query entry each. */
|
|
@@ -235,5 +245,7 @@ export interface ByColourParams {
|
|
|
235
245
|
* drop.
|
|
236
246
|
*/
|
|
237
247
|
family?: string;
|
|
248
|
+
/** Narrow retrieval inside the server query; absent means the whole graph. */
|
|
249
|
+
corpusScope?: CorpusScope;
|
|
238
250
|
limit?: number;
|
|
239
251
|
}
|
package/client-browse.d.ts
CHANGED
|
@@ -33,6 +33,17 @@ export type BrowsePageFetcher = (params: BrowseParams, signal?: AbortSignal) =>
|
|
|
33
33
|
* route and never reached it from here, so the fan deck was unreachable through
|
|
34
34
|
* the published client while the server had supported it all along. When the
|
|
35
35
|
* route gains a param, it gains a line HERE too, or it is not shipped.
|
|
36
|
+
*
|
|
37
|
+
* It happened again, and twice: `filters` shipped with MG-1576 and `facetCounts`
|
|
38
|
+
* with MG-1590, and neither had a line here. Attribute filtering — the thing
|
|
39
|
+
* MG-1588 spent a materialized view making fast — was therefore unreachable
|
|
40
|
+
* through the published client for as long as it existed on the route. Both are
|
|
41
|
+
* enrolled below, along with MG-1593's `orderBy` and `returnAttributes`.
|
|
42
|
+
*
|
|
43
|
+
* `filters` is a REPEATED parameter and `facetCounts` a comma list, because that
|
|
44
|
+
* is what the route reads: the transport appends an array once per element, so
|
|
45
|
+
* the array shape and the comma-joined string are each doing exactly what the
|
|
46
|
+
* server expects rather than being two spellings of one idea.
|
|
36
47
|
*/
|
|
37
48
|
export declare function browseQuery(params: BrowseParams): {
|
|
38
49
|
sort: BrowseParams["sort"];
|
|
@@ -41,7 +52,11 @@ export declare function browseQuery(params: BrowseParams): {
|
|
|
41
52
|
count: BrowseParams["count"];
|
|
42
53
|
types: BrowseParams["types"];
|
|
43
54
|
brands: BrowseParams["brands"];
|
|
55
|
+
filters: string[] | undefined;
|
|
56
|
+
facetCounts: string | undefined;
|
|
44
57
|
corpusScope: BrowseParams["corpusScope"];
|
|
58
|
+
orderBy: string | undefined;
|
|
59
|
+
returnAttributes: string | undefined;
|
|
45
60
|
};
|
|
46
61
|
/**
|
|
47
62
|
* Walk every page from `params.cursor` onward.
|
package/client-browse.js
CHANGED
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
* route and never reached it from here, so the fan deck was unreachable through
|
|
9
9
|
* the published client while the server had supported it all along. When the
|
|
10
10
|
* route gains a param, it gains a line HERE too, or it is not shipped.
|
|
11
|
+
*
|
|
12
|
+
* It happened again, and twice: `filters` shipped with MG-1576 and `facetCounts`
|
|
13
|
+
* with MG-1590, and neither had a line here. Attribute filtering — the thing
|
|
14
|
+
* MG-1588 spent a materialized view making fast — was therefore unreachable
|
|
15
|
+
* through the published client for as long as it existed on the route. Both are
|
|
16
|
+
* enrolled below, along with MG-1593's `orderBy` and `returnAttributes`.
|
|
17
|
+
*
|
|
18
|
+
* `filters` is a REPEATED parameter and `facetCounts` a comma list, because that
|
|
19
|
+
* is what the route reads: the transport appends an array once per element, so
|
|
20
|
+
* the array shape and the comma-joined string are each doing exactly what the
|
|
21
|
+
* server expects rather than being two spellings of one idea.
|
|
11
22
|
*/
|
|
12
23
|
export function browseQuery(params) {
|
|
13
24
|
return {
|
|
@@ -17,7 +28,15 @@ export function browseQuery(params) {
|
|
|
17
28
|
count: params.count,
|
|
18
29
|
types: params.types,
|
|
19
30
|
brands: params.brands,
|
|
31
|
+
filters: params.filters?.map((entry) => JSON.stringify(entry)),
|
|
32
|
+
facetCounts: params.facetCounts?.join(","),
|
|
20
33
|
corpusScope: params.corpusScope,
|
|
34
|
+
// `orderBy` is JSON-encoded and `returnAttributes` comma-joined because that
|
|
35
|
+
// is what the route reads — the same two transports `filters` and
|
|
36
|
+
// `facetCounts` already use. Enrolled HERE at the same time they shipped on
|
|
37
|
+
// the route, which is the thing the three misses above did not do.
|
|
38
|
+
orderBy: params.orderBy ? JSON.stringify(params.orderBy) : undefined,
|
|
39
|
+
returnAttributes: params.returnAttributes?.join(","),
|
|
21
40
|
};
|
|
22
41
|
}
|
|
23
42
|
/**
|
package/client.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { MatchStreamEvent } from "./match-stream-types.js";
|
|
|
19
19
|
import type { MatchInput, MatchResponse } from "./match-types.js";
|
|
20
20
|
import type { MaterialAssetProjection } from "./material-asset-types.js";
|
|
21
21
|
import type { MaterialBlockProjection } from "./material-block-types.js";
|
|
22
|
+
import type { MaterialQueryPlan } from "./material-query-plan-types.js";
|
|
22
23
|
import type { AppendProjectEventRequest, CreateProjectRequest, ProjectEventReceipt, ProjectListResponse, ProjectState } from "./project-types.js";
|
|
23
24
|
import { resolvePhrasesRequest as resolvePhrases } from "./resolve-client.js";
|
|
24
25
|
import type { CapabilityManifestResponse, CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, ListRunsOptions, PublicRunListResponse, PublicRunResponse, ResumeRunOptions, RunEvent, SourceMaterialBatchDetail, SourceMaterialBatchReceipt, StreamRunEventsOptions, UploadSourceMaterialInput } from "./run-types.js";
|
|
@@ -127,17 +128,13 @@ export declare class MaterialGraphClient {
|
|
|
127
128
|
* {@link MaterialGraphClient.match} for a photo, {@link MaterialGraphClient.pairings}
|
|
128
129
|
* for what goes WITH something, {@link MaterialGraphClient.suggest} for typeahead.
|
|
129
130
|
*
|
|
130
|
-
* The full search primitive
|
|
131
|
-
* specification
|
|
132
|
-
*
|
|
133
|
-
* a term match (`{ attributeId, term }`), a presence check
|
|
134
|
-
* (`{ attributeId, hasValue: true }`), or a certification programme
|
|
135
|
-
* (`{ attributeId: "certification_programme", term }`). This is the seam for
|
|
136
|
-
* "a GREENGUARD-certified alternative to THIS material" (seed by `similarTo`,
|
|
137
|
-
* gate by `filters`) or "the same fire class, with an EPD on record". Build the
|
|
138
|
-
* filters with the `certificationProgramme` / `specTerm` / `hasSpec` helpers.
|
|
131
|
+
* The full POST search primitive. `input.filters` layers typed hard
|
|
132
|
+
* specification gates onto ranked colour/visual/lexical/seed search. Build
|
|
133
|
+
* them with `certificationProgramme`, `specTerm`, or `hasSpec`.
|
|
139
134
|
*/
|
|
140
135
|
search(input: SearchInput, signal?: AbortSignal): Promise<SearchResponse>;
|
|
136
|
+
/** Whole materials ask → inspectable meanings and complete safe search requests. */
|
|
137
|
+
planQuery(query: string, signal?: AbortSignal): Promise<MaterialQueryPlan>;
|
|
141
138
|
/**
|
|
142
139
|
* The same question as {@link MaterialGraphClient.search}; the difference is
|
|
143
140
|
* transport — a cacheable, link-shareable GET for queries that must survive in
|
package/client.js
CHANGED
|
@@ -157,19 +157,17 @@ export class MaterialGraphClient {
|
|
|
157
157
|
* {@link MaterialGraphClient.match} for a photo, {@link MaterialGraphClient.pairings}
|
|
158
158
|
* for what goes WITH something, {@link MaterialGraphClient.suggest} for typeahead.
|
|
159
159
|
*
|
|
160
|
-
* The full search primitive
|
|
161
|
-
* specification
|
|
162
|
-
*
|
|
163
|
-
* a term match (`{ attributeId, term }`), a presence check
|
|
164
|
-
* (`{ attributeId, hasValue: true }`), or a certification programme
|
|
165
|
-
* (`{ attributeId: "certification_programme", term }`). This is the seam for
|
|
166
|
-
* "a GREENGUARD-certified alternative to THIS material" (seed by `similarTo`,
|
|
167
|
-
* gate by `filters`) or "the same fire class, with an EPD on record". Build the
|
|
168
|
-
* filters with the `certificationProgramme` / `specTerm` / `hasSpec` helpers.
|
|
160
|
+
* The full POST search primitive. `input.filters` layers typed hard
|
|
161
|
+
* specification gates onto ranked colour/visual/lexical/seed search. Build
|
|
162
|
+
* them with `certificationProgramme`, `specTerm`, or `hasSpec`.
|
|
169
163
|
*/
|
|
170
164
|
search(input, signal) {
|
|
171
165
|
return this.#post("search_materials_post", input, signal);
|
|
172
166
|
}
|
|
167
|
+
/** Whole materials ask → inspectable meanings and complete safe search requests. */
|
|
168
|
+
planQuery(query, signal) {
|
|
169
|
+
return this.#post("plan_material_query", { query }, signal);
|
|
170
|
+
}
|
|
173
171
|
/**
|
|
174
172
|
* The same question as {@link MaterialGraphClient.search}; the difference is
|
|
175
173
|
* transport — a cacheable, link-shareable GET for queries that must survive in
|
|
@@ -235,6 +233,7 @@ export class MaterialGraphClient {
|
|
|
235
233
|
*/
|
|
236
234
|
byColour(params, signal) {
|
|
237
235
|
return this.#get("search_by_colour", {
|
|
236
|
+
q: params.q,
|
|
238
237
|
hex: params.hex,
|
|
239
238
|
colour: params.colour,
|
|
240
239
|
l: params.l,
|
|
@@ -243,6 +242,7 @@ export class MaterialGraphClient {
|
|
|
243
242
|
category: params.category,
|
|
244
243
|
brand: params.brand,
|
|
245
244
|
family: params.family,
|
|
245
|
+
corpusScope: params.corpusScope,
|
|
246
246
|
limit: params.limit,
|
|
247
247
|
}, signal);
|
|
248
248
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The corpus scope vocabulary, declared ONCE for the whole client.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this file exists ──
|
|
5
|
+
* Until MG-1168 the union was spelled inline at six places across the SDK, and
|
|
6
|
+
* five of them had gone stale: `designShopOrderable` shipped in MG-1144 and
|
|
7
|
+
* reached only the search request type, so a TypeScript consumer could send the
|
|
8
|
+
* scope to `/search` and not to `/browse` — against a server that had accepted
|
|
9
|
+
* it on both since the day it landed. The narrowed RECEIPT types were worse
|
|
10
|
+
* than the request ones, because a caller could not even name the scope it got
|
|
11
|
+
* back.
|
|
12
|
+
*
|
|
13
|
+
* That is the third hop of the reachability checklist failing quietly: server
|
|
14
|
+
* correct, published client unable to reach the surface. One alias means the
|
|
15
|
+
* next member is one edit here rather than six edits nobody is reminded to
|
|
16
|
+
* make. `corpus-scope-parity.test.ts` in `apps/web/config` fails if this file
|
|
17
|
+
* falls behind `CORPUS_SCOPES`.
|
|
18
|
+
*
|
|
19
|
+
* ── Why `OpenEnum` ──
|
|
20
|
+
* The server may add a scope before a consumer upgrades. `OpenEnum` keeps the
|
|
21
|
+
* known members as autocomplete while still accepting a string the server knows
|
|
22
|
+
* and this build does not, so a new scope is not a compile error in a client
|
|
23
|
+
* that simply has not been republished yet.
|
|
24
|
+
*/
|
|
25
|
+
import { type OpenEnum } from "./types.js";
|
|
26
|
+
/**
|
|
27
|
+
* Every corpus a caller may ask for.
|
|
28
|
+
*
|
|
29
|
+
* `"all"` (and absence) is the WHOLE GRAPH — every material MaterialGraph
|
|
30
|
+
* knows, orderable or not. Absence is never a narrower default.
|
|
31
|
+
*
|
|
32
|
+
* Three members gate on CHANNEL membership, which is what "can someone buy
|
|
33
|
+
* this" means: `"materialBankNaOrderable"` (Material Bank North America),
|
|
34
|
+
* `"materialBankEuOrderable"` (Material Bank Europe) and
|
|
35
|
+
* `"designShopOrderable"` (DesignShop, with the products DesignShop hides from
|
|
36
|
+
* browse excluded).
|
|
37
|
+
*
|
|
38
|
+
* `"materialBankOrderable"` is NOT one of them, despite the name. It selects
|
|
39
|
+
* materials carrying a Material Bank identity — PROVENANCE, not a storefront —
|
|
40
|
+
* and admits about 98% of the graph. Use it to exclude the crawl-sourced brands
|
|
41
|
+
* that were never in the Material Bank feed; if you mean orderable, use one of
|
|
42
|
+
* the three above.
|
|
43
|
+
*/
|
|
44
|
+
export type CorpusScope = OpenEnum<"all" | "designShopOrderable" | "materialBankEuOrderable" | "materialBankNaOrderable" | "materialBankOrderable">;
|
|
45
|
+
/**
|
|
46
|
+
* The corpus a response AND its counts were drawn from. Present only when a
|
|
47
|
+
* narrowing scope applied — absence means the whole graph. Read it alongside
|
|
48
|
+
* the counts: a short page under a scope is a small SCOPED corpus, never a
|
|
49
|
+
* truncated one.
|
|
50
|
+
*/
|
|
51
|
+
export interface CorpusScopeReceipt {
|
|
52
|
+
scope: CorpusScope;
|
|
53
|
+
/** What the scope admits, in one sentence, for a human reader. */
|
|
54
|
+
description: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The corpus scope vocabulary, declared ONCE for the whole client.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this file exists ──
|
|
5
|
+
* Until MG-1168 the union was spelled inline at six places across the SDK, and
|
|
6
|
+
* five of them had gone stale: `designShopOrderable` shipped in MG-1144 and
|
|
7
|
+
* reached only the search request type, so a TypeScript consumer could send the
|
|
8
|
+
* scope to `/search` and not to `/browse` — against a server that had accepted
|
|
9
|
+
* it on both since the day it landed. The narrowed RECEIPT types were worse
|
|
10
|
+
* than the request ones, because a caller could not even name the scope it got
|
|
11
|
+
* back.
|
|
12
|
+
*
|
|
13
|
+
* That is the third hop of the reachability checklist failing quietly: server
|
|
14
|
+
* correct, published client unable to reach the surface. One alias means the
|
|
15
|
+
* next member is one edit here rather than six edits nobody is reminded to
|
|
16
|
+
* make. `corpus-scope-parity.test.ts` in `apps/web/config` fails if this file
|
|
17
|
+
* falls behind `CORPUS_SCOPES`.
|
|
18
|
+
*
|
|
19
|
+
* ── Why `OpenEnum` ──
|
|
20
|
+
* The server may add a scope before a consumer upgrades. `OpenEnum` keeps the
|
|
21
|
+
* known members as autocomplete while still accepting a string the server knows
|
|
22
|
+
* and this build does not, so a new scope is not a compile error in a client
|
|
23
|
+
* that simply has not been republished yet.
|
|
24
|
+
*/
|
|
25
|
+
import {} from "./types.js";
|
package/demand-types.d.ts
CHANGED
|
@@ -95,6 +95,26 @@ export interface CoordinateCompanion {
|
|
|
95
95
|
imageUrl: string | null;
|
|
96
96
|
brandSlug: string;
|
|
97
97
|
productId: ProductId;
|
|
98
|
+
/**
|
|
99
|
+
* How this companion relates to the SEED SET — the closest relation that
|
|
100
|
+
* applies to any seed.
|
|
101
|
+
*
|
|
102
|
+
* `companion` is foreign to every seed: a maker the caller did not pick.
|
|
103
|
+
* `brand_sibling` shares a brand with some seed. `colourway` is a seed's own
|
|
104
|
+
* product restyled, and completes nothing.
|
|
105
|
+
*
|
|
106
|
+
* READ IT BEFORE CLAIMING BREADTH. Measured across the live edges (MG-1007),
|
|
107
|
+
* 70.6% are same-brand and 24.3% are the seed's own product in another
|
|
108
|
+
* colourway; on a two-seed rail, 27 of 40 companions came from the seeds' own
|
|
109
|
+
* two brands. A surface headed "makers you haven't picked" that does not
|
|
110
|
+
* filter on this is making a claim the evidence does not support — the
|
|
111
|
+
* measured case showed two distinct makers under a heading promising seven.
|
|
112
|
+
*
|
|
113
|
+
* The server has always sent this field; it was absent from this interface
|
|
114
|
+
* until 2026-08-23, so consumers could not see it and hand-rolled the same
|
|
115
|
+
* comparison against `brandSlug` themselves.
|
|
116
|
+
*/
|
|
117
|
+
relation: "companion" | "brand_sibling" | "colourway";
|
|
98
118
|
totalLift: number;
|
|
99
119
|
liftLabel: string;
|
|
100
120
|
seedMatchCount: number;
|
package/errors.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface ProblemDetail {
|
|
|
17
17
|
suggestions?: string[];
|
|
18
18
|
doc_uri?: string;
|
|
19
19
|
issues?: SanitizedIssue[];
|
|
20
|
+
/**
|
|
21
|
+
* MaterialGraph's documented RFC 7807 extension members, sanitized by the
|
|
22
|
+
* same rules as every other nested value here. Absent when the response did
|
|
23
|
+
* not carry them. See {@link MATERIALGRAPH_PROBLEM_EXTENSIONS}.
|
|
24
|
+
*/
|
|
25
|
+
readonly [extension: string]: unknown;
|
|
20
26
|
}
|
|
21
27
|
/** Normalized residual `{ error }` body. Unknown response fields are discarded. */
|
|
22
28
|
export interface SimpleErrorBody {
|
|
@@ -61,6 +67,29 @@ interface TransportErrorInput extends MaterialGraphRequestMetadata {
|
|
|
61
67
|
cause: unknown;
|
|
62
68
|
callerSignal?: AbortSignal;
|
|
63
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* MaterialGraph's documented problem EXTENSION members — the only non-standard
|
|
72
|
+
* keys this client retains.
|
|
73
|
+
*
|
|
74
|
+
* ── Why an allow-list and not "keep everything" ──
|
|
75
|
+
* Dropping unknown members is a deliberate decision here, pinned by a test that
|
|
76
|
+
* sends `unknown_top_level: "must-not-be-retained"` and asserts it is gone. A
|
|
77
|
+
* client should not echo arbitrary server-supplied keys into an object the
|
|
78
|
+
* caller trusts. That decision stands.
|
|
79
|
+
*
|
|
80
|
+
* ── Why not "drop everything", which is what it used to do ──
|
|
81
|
+
* MaterialGraph sends these members precisely so a caller can BRANCH on a
|
|
82
|
+
* refusal rather than parse its prose, and dropping them silently forced the
|
|
83
|
+
* opposite. A consumer reported it on 2026-08-21: they received
|
|
84
|
+
* `populated_alternatives` on the wire, found it absent from the thrown error,
|
|
85
|
+
* and were left matching a regex against `detail` — the exact coupling the
|
|
86
|
+
* field was added to remove. All ten members below were unreachable.
|
|
87
|
+
*
|
|
88
|
+
* So: the server's own contract is retained, and nothing else is. Adding a
|
|
89
|
+
* member to a problem response means adding it here too, and
|
|
90
|
+
* `problem-extension-parity.test.ts` fails when the two disagree.
|
|
91
|
+
*/
|
|
92
|
+
export declare const MATERIALGRAPH_PROBLEM_EXTENSIONS: readonly ["content_length", "did_you_mean", "empty_brands", "limit_bytes", "missing_env", "orderable_attributes", "populated_alternatives", "unknown_attributes", "unknown_brands", "unknown_fields", "unorderable_attribute", "valid_group_keys", "valid_type_keys"];
|
|
64
93
|
/** The one error class thrown by all SDK transport and HTTP failures. */
|
|
65
94
|
export declare class MaterialGraphError extends Error {
|
|
66
95
|
readonly kind: MaterialGraphErrorKind;
|
package/errors.js
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
/** Error and rate-limit contracts for the retry-free MaterialGraph SDK. */
|
|
2
2
|
const RETRIABLE_STATUS = new Set([502, 503, 504]);
|
|
3
|
+
/**
|
|
4
|
+
* MaterialGraph's documented problem EXTENSION members — the only non-standard
|
|
5
|
+
* keys this client retains.
|
|
6
|
+
*
|
|
7
|
+
* ── Why an allow-list and not "keep everything" ──
|
|
8
|
+
* Dropping unknown members is a deliberate decision here, pinned by a test that
|
|
9
|
+
* sends `unknown_top_level: "must-not-be-retained"` and asserts it is gone. A
|
|
10
|
+
* client should not echo arbitrary server-supplied keys into an object the
|
|
11
|
+
* caller trusts. That decision stands.
|
|
12
|
+
*
|
|
13
|
+
* ── Why not "drop everything", which is what it used to do ──
|
|
14
|
+
* MaterialGraph sends these members precisely so a caller can BRANCH on a
|
|
15
|
+
* refusal rather than parse its prose, and dropping them silently forced the
|
|
16
|
+
* opposite. A consumer reported it on 2026-08-21: they received
|
|
17
|
+
* `populated_alternatives` on the wire, found it absent from the thrown error,
|
|
18
|
+
* and were left matching a regex against `detail` — the exact coupling the
|
|
19
|
+
* field was added to remove. All ten members below were unreachable.
|
|
20
|
+
*
|
|
21
|
+
* So: the server's own contract is retained, and nothing else is. Adding a
|
|
22
|
+
* member to a problem response means adding it here too, and
|
|
23
|
+
* `problem-extension-parity.test.ts` fails when the two disagree.
|
|
24
|
+
*/
|
|
25
|
+
export const MATERIALGRAPH_PROBLEM_EXTENSIONS = [
|
|
26
|
+
"content_length",
|
|
27
|
+
"did_you_mean",
|
|
28
|
+
"empty_brands",
|
|
29
|
+
"limit_bytes",
|
|
30
|
+
"missing_env",
|
|
31
|
+
"orderable_attributes",
|
|
32
|
+
"populated_alternatives",
|
|
33
|
+
"unknown_attributes",
|
|
34
|
+
"unknown_brands",
|
|
35
|
+
"unknown_fields",
|
|
36
|
+
"unorderable_attribute",
|
|
37
|
+
"valid_group_keys",
|
|
38
|
+
"valid_type_keys",
|
|
39
|
+
];
|
|
40
|
+
const PROBLEM_EXTENSION_KEYS = new Set(MATERIALGRAPH_PROBLEM_EXTENSIONS);
|
|
3
41
|
const MAX_UNEXPECTED_TEXT = 768;
|
|
4
42
|
const MAX_EXTENSION_DEPTH = 8;
|
|
5
43
|
const MAX_EXTENSION_ITEMS = 100;
|
|
@@ -89,7 +127,12 @@ function normalizeProblem(body, secrets) {
|
|
|
89
127
|
}
|
|
90
128
|
const suggestions = sanitizeSuggestions(body.suggestions, secrets);
|
|
91
129
|
const issues = sanitizeIssues(body.issues, secrets);
|
|
130
|
+
// Extensions FIRST so a malformed one can never shadow a standard member
|
|
131
|
+
// below. `sanitizeRecord` is the existing redaction and depth/item caps —
|
|
132
|
+
// this is that machinery finally being reached, not a second sanitizer.
|
|
133
|
+
const extensions = sanitizeRecord(Object.fromEntries(Object.entries(body).filter(([key]) => PROBLEM_EXTENSION_KEYS.has(key))), secrets, 0);
|
|
92
134
|
return {
|
|
135
|
+
...extensions,
|
|
93
136
|
type: safeString(body.type, secrets),
|
|
94
137
|
title: safeString(body.title, secrets),
|
|
95
138
|
status: body.status,
|
package/exploration-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
1
2
|
import type { VariantId } from "./identifiers.js";
|
|
2
3
|
export type GraphOperator = "colourways" | "same_category" | "still_listed" | "looks_like" | "same_colour" | "pairs_with" | "often_specified_with" | "substitutes_for" | "more_durable" | "outdoor_suitable" | "lower_cost" | "more_sustainable";
|
|
3
4
|
export interface GraphOperatorAvailability {
|
|
@@ -14,7 +15,7 @@ export interface GraphOperatorEvidence {
|
|
|
14
15
|
}
|
|
15
16
|
export interface CandidateReadInput {
|
|
16
17
|
operator: "looks_like";
|
|
17
|
-
corpusScope?:
|
|
18
|
+
corpusScope?: CorpusScope;
|
|
18
19
|
limit?: number;
|
|
19
20
|
excludeVariantIds?: VariantId[];
|
|
20
21
|
}
|
|
@@ -62,10 +63,7 @@ export interface CandidateSeedEvidence {
|
|
|
62
63
|
}
|
|
63
64
|
export interface CandidateReadReceipt {
|
|
64
65
|
operator: "looks_like";
|
|
65
|
-
corpusScope:
|
|
66
|
-
scope: "all" | "materialBankOrderable";
|
|
67
|
-
description: string;
|
|
68
|
-
} | null;
|
|
66
|
+
corpusScope: CorpusScopeReceipt | null;
|
|
69
67
|
requestedLimit: number;
|
|
70
68
|
returnedCount: number;
|
|
71
69
|
exclusions: {
|
package/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { PROJECT_EVENT_KINDS } from "./project-types.js";
|
|
|
14
14
|
export type * from "./project-types.js";
|
|
15
15
|
export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
|
|
16
16
|
export type * from "./run-events.js";
|
|
17
|
-
export { MaterialGraphError } from "./errors.js";
|
|
17
|
+
export { MATERIALGRAPH_PROBLEM_EXTENSIONS, MaterialGraphError, } from "./errors.js";
|
|
18
18
|
export type { MaterialGraphErrorInit, MaterialGraphErrorKind, MaterialGraphRequestMetadata, ProblemDetail, RateLimit, SanitizedErrorValue, SanitizedIssue, SimpleErrorBody, } from "./errors.js";
|
|
19
19
|
export type * from "./types.js";
|
|
20
20
|
export type * from "./match-types.js";
|
|
@@ -28,6 +28,8 @@ export { ColoursClient } from "./colours-client.js";
|
|
|
28
28
|
export type { ColoursTransport } from "./colours-client.js";
|
|
29
29
|
export type * from "./colour-aggregate-types.js";
|
|
30
30
|
export type * from "./palette-search-types.js";
|
|
31
|
+
export type * from "./query-understanding-types.js";
|
|
32
|
+
export type * from "./material-query-plan-types.js";
|
|
31
33
|
export type * from "./association-types.js";
|
|
32
34
|
export type * from "./brand-types.js";
|
|
33
35
|
export type * from "./fan-deck-types.js";
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
|
|
|
4
4
|
export { VARY_AXES } from "./composition-types.js";
|
|
5
5
|
export { PROJECT_EVENT_KINDS } from "./project-types.js";
|
|
6
6
|
export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
|
|
7
|
-
export { MaterialGraphError } from "./errors.js";
|
|
7
|
+
export { MATERIALGRAPH_PROBLEM_EXTENSIONS, MaterialGraphError, } from "./errors.js";
|
|
8
8
|
export { MATCH_IMAGE_TYPES, MATCH_OUTCOMES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
|
|
9
9
|
export { CodesClient } from "./codes-client.js";
|
|
10
10
|
export { ColoursClient } from "./colours-client.js";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** Hand-mirrored wire types for `POST /api/v1/search/plan`. */
|
|
2
|
+
import type { QuerySense } from "./query-understanding-types.js";
|
|
3
|
+
import type { ResolvedMeaning } from "./resolve-types.js";
|
|
4
|
+
import type { SearchInput } from "./search-request-types.js";
|
|
5
|
+
import type { OpenEnum } from "./types.js";
|
|
6
|
+
export type MaterialQueryVocabularyKind = OpenEnum<"attribute" | "dictionary_term" | "inline_enum" | "product_type" | "product_type_group">;
|
|
7
|
+
export interface MaterialQuerySpan {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MaterialQueryOwner {
|
|
13
|
+
attributeId: string;
|
|
14
|
+
attributeName: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MaterialQueryLifecycle {
|
|
17
|
+
status: string;
|
|
18
|
+
introducedIn?: string;
|
|
19
|
+
deprecatedIn?: string;
|
|
20
|
+
supersededBy?: string;
|
|
21
|
+
}
|
|
22
|
+
export type MaterialQueryOntologyContext = {
|
|
23
|
+
description: string;
|
|
24
|
+
valueKind: string;
|
|
25
|
+
valueDictionaryId?: string;
|
|
26
|
+
} | {
|
|
27
|
+
dictionaryId: string;
|
|
28
|
+
dictionaryName: string;
|
|
29
|
+
owningAttributes: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}>;
|
|
33
|
+
lifecycle: MaterialQueryLifecycle;
|
|
34
|
+
} | {
|
|
35
|
+
attributeId: string;
|
|
36
|
+
attributeName: string;
|
|
37
|
+
} | {
|
|
38
|
+
description: string;
|
|
39
|
+
lifecycle: MaterialQueryLifecycle;
|
|
40
|
+
parentProductTypeGroupId?: string;
|
|
41
|
+
} | {
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
export interface MaterialQueryApplicability {
|
|
45
|
+
status: OpenEnum<"all" | "some" | "none" | "unknown">;
|
|
46
|
+
categoryId?: string;
|
|
47
|
+
productTypeIds: string[];
|
|
48
|
+
applicableProductTypeIds: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface MaterialQueryCoverage {
|
|
51
|
+
variants: number;
|
|
52
|
+
basis: OpenEnum<"facet_index">;
|
|
53
|
+
scope: OpenEnum<"corpus">;
|
|
54
|
+
}
|
|
55
|
+
export interface MaterialQueryReachability {
|
|
56
|
+
status: OpenEnum<"positive" | "zero" | "unknown">;
|
|
57
|
+
categoryId: string;
|
|
58
|
+
basis: OpenEnum<"bounded_probe" | "unmeasured">;
|
|
59
|
+
}
|
|
60
|
+
export interface MaterialQueryCandidate {
|
|
61
|
+
kind: MaterialQueryVocabularyKind;
|
|
62
|
+
subjectType: MaterialQueryVocabularyKind;
|
|
63
|
+
subjectId: string;
|
|
64
|
+
label: string;
|
|
65
|
+
context: MaterialQueryOntologyContext;
|
|
66
|
+
score: number;
|
|
67
|
+
tier: OpenEnum<"exact_id" | "exact_alias" | "lexical" | "semantic">;
|
|
68
|
+
evidence: {
|
|
69
|
+
source: OpenEnum<"registry" | "query_domain_embedding">;
|
|
70
|
+
span: MaterialQuerySpan;
|
|
71
|
+
};
|
|
72
|
+
outcome: OpenEnum<"recognized" | "suggested" | "executable" | "ambiguous" | "refused">;
|
|
73
|
+
owners: MaterialQueryOwner[];
|
|
74
|
+
selectedOwner?: MaterialQueryOwner;
|
|
75
|
+
applicability?: MaterialQueryApplicability;
|
|
76
|
+
coverage?: MaterialQueryCoverage;
|
|
77
|
+
reachability?: MaterialQueryReachability;
|
|
78
|
+
reason?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface MaterialQueryCriterion {
|
|
81
|
+
source: {
|
|
82
|
+
subjectType: MaterialQueryVocabularyKind;
|
|
83
|
+
subjectId: string;
|
|
84
|
+
span: MaterialQuerySpan;
|
|
85
|
+
};
|
|
86
|
+
attribute: {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
};
|
|
90
|
+
ownerAlternatives: MaterialQueryOwner[];
|
|
91
|
+
operator: OpenEnum<"term" | "presence" | "numeric_range">;
|
|
92
|
+
term?: string;
|
|
93
|
+
min?: number;
|
|
94
|
+
max?: number;
|
|
95
|
+
coverage: MaterialQueryCoverage;
|
|
96
|
+
applicability: MaterialQueryApplicability;
|
|
97
|
+
reachability?: MaterialQueryReachability;
|
|
98
|
+
}
|
|
99
|
+
export type MaterialQueryStandardMeaning = ResolvedMeaning | QuerySense;
|
|
100
|
+
export type MaterialQueryAmbiguityAlternative = (MaterialQueryOwner & {
|
|
101
|
+
kind: OpenEnum<"owner">;
|
|
102
|
+
}) | {
|
|
103
|
+
kind: OpenEnum<"subject">;
|
|
104
|
+
subjectType: MaterialQueryVocabularyKind;
|
|
105
|
+
subjectId: string;
|
|
106
|
+
label: string;
|
|
107
|
+
} | {
|
|
108
|
+
kind: OpenEnum<"standard_meaning">;
|
|
109
|
+
meaning: MaterialQueryStandardMeaning;
|
|
110
|
+
};
|
|
111
|
+
export interface MaterialQueryPlan {
|
|
112
|
+
version: OpenEnum<"1">;
|
|
113
|
+
verdict: OpenEnum<"executable" | "partial" | "refused">;
|
|
114
|
+
rawQuery: string;
|
|
115
|
+
normalisedQuery: string;
|
|
116
|
+
registry: {
|
|
117
|
+
corpusSize: number;
|
|
118
|
+
contentHash: string;
|
|
119
|
+
};
|
|
120
|
+
candidates: MaterialQueryCandidate[];
|
|
121
|
+
category: {
|
|
122
|
+
key: string;
|
|
123
|
+
name: string;
|
|
124
|
+
level: OpenEnum<"group" | "type">;
|
|
125
|
+
parentKey: string | null;
|
|
126
|
+
source: {
|
|
127
|
+
subjectType: MaterialQueryVocabularyKind;
|
|
128
|
+
subjectId: string;
|
|
129
|
+
span: MaterialQuerySpan;
|
|
130
|
+
};
|
|
131
|
+
} | null;
|
|
132
|
+
criteria: MaterialQueryCriterion[];
|
|
133
|
+
colour: {
|
|
134
|
+
family?: string;
|
|
135
|
+
undertone?: string;
|
|
136
|
+
} | null;
|
|
137
|
+
residualText: string;
|
|
138
|
+
unspentSpans: Array<MaterialQuerySpan & {
|
|
139
|
+
reason: OpenEnum<"residual" | "ambiguous" | "refused">;
|
|
140
|
+
}>;
|
|
141
|
+
ambiguities: Array<{
|
|
142
|
+
subjectType: MaterialQueryVocabularyKind;
|
|
143
|
+
subjectId: string;
|
|
144
|
+
span: MaterialQuerySpan;
|
|
145
|
+
reason: string;
|
|
146
|
+
alternatives: MaterialQueryAmbiguityAlternative[];
|
|
147
|
+
}>;
|
|
148
|
+
standardMeanings: Array<{
|
|
149
|
+
span: MaterialQuerySpan;
|
|
150
|
+
meanings: MaterialQueryStandardMeaning[];
|
|
151
|
+
}>;
|
|
152
|
+
/** Complete ordinary-search requests authored by MaterialGraph. Execute verbatim. */
|
|
153
|
+
requests: SearchInput[];
|
|
154
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/operations.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const MATERIALGRAPH_API_VERSION = "0.7.0";
|
|
2
|
-
declare const OPERATION_DEFINITIONS: readonly [readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["searchGet", "GET", "/api/v1/search", "search_materials_get"], readonly ["match", "POST", "/api/v1/search/match", "match_material"], readonly ["byColour", "GET", "/api/v1/search/colours", "search_by_colour"], readonly ["material", "GET", "/api/v1/materials/{variantId}", "get_material"], readonly ["siblings", "GET", "/api/v1/materials/{variantId}/siblings", "get_material_siblings"], readonly ["materialBlocks", "GET", "/api/v1/materials/{variantId}/blocks", "list_material_blocks"], readonly ["materialAssets", "GET", "/api/v1/materials/{variantId}/assets", "list_material_assets"], readonly ["exploration.operators", "GET", "/api/v1/materials/{variantId}/operators", "list_material_operators"], readonly ["exploration.candidates", "POST", "/api/v1/materials/{variantId}/candidates", "read_material_candidates"], readonly ["schema", "GET", "/api/v1/schema/blocks", "list_schema_blocks"], readonly ["schema", "GET", "/api/v1/schema/attributes", "list_schema_attributes"], readonly ["schema", "GET", "/api/v1/schema/verticals", "list_schema_verticals"], readonly ["schema", "GET", "/api/v1/schema/product-types", "list_schema_product_types"], readonly ["schema", "GET", "/api/v1/schema/units", "list_schema_units"], readonly ["schema", "GET", "/api/v1/schema/value-dictionaries", "list_schema_value_dictionaries"], readonly ["schema", "GET", "/api/v1/schema/source-requirements", "list_schema_source_requirements"], readonly ["schema", "GET", "/api/v1/schema/product-type-templates", "list_schema_product_type_templates"], readonly ["schema", "GET", "/api/v1/schema/mappings", "list_schema_mappings"], readonly ["pairings", "GET", "/api/v1/materials/{variantId}/pairings", "get_material_pairings"], readonly ["associatedFinishes", "GET", "/api/v1/associated/finishes", "list_associated_finishes"], readonly ["associatedProducts", "GET", "/api/v1/associated/products", "list_associated_products"], readonly ["materials", "POST", "/api/v1/materials/batch", "get_materials_batch_post"], readonly ["materialsCached", "GET", "/api/v1/materials/batch", "get_materials_batch_get"], readonly ["browse", "GET", "/api/v1/search/browse", "search_browse_by_colour"], readonly ["colourFacets", "GET", "/api/v1/colours/facets", "get_colour_facets"], readonly ["colours.profiles", "GET", "/api/v1/colours/profiles", "get_category_colour_profiles"], readonly ["colours.stats", "GET", "/api/v1/colours/stats", "get_colour_stats"], readonly ["resolveColour", "GET", "/api/v1/search/colours/resolve", "resolve_colour_name"], readonly ["suggestColour", "GET", "/api/v1/search/colours/suggest", "suggest_colour_names"], readonly ["suggest", "GET", "/api/v1/search/suggest", "search_suggest"], readonly ["resolveAttribute", "GET", "/api/v1/catalog/attributes/resolve", "catalog_resolve_attribute"], readonly ["resolveAttributeValue", "GET", "/api/v1/catalog/attributes/{attributeId}/values/resolve", "catalog_resolve_value"], readonly ["searchVocabulary", "GET", "/api/v1/catalog/vocabulary/search", "catalog_vocabulary_search"], readonly ["resolve", "POST", "/api/v1/resolve", "resolve_phrases"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["whoami", "GET", "/api/v1/whoami", "whoami"], readonly ["coordinate", "POST", "/api/v1/pairings/coordinate", "coordinate_materials"], readonly ["demand", "GET", "/api/v1/demand", "get_material_demand"], readonly ["colours.palette", "POST", "/api/v1/search/palettes", "search_by_palette"], readonly ["extractPalette", "POST", "/api/v1/search/palettes/extract", "extract_image_palette"], readonly ["colours.gradient", "POST", "/api/v1/search/gradients", "search_by_gradient"], readonly ["specificationFacets", "GET", "/api/v1/catalog/specifications/facets", "list_spec_facets"], readonly ["attributeValues", "GET", "/api/v1/catalog/attributes/{attributeId}/values", "list_attribute_values"], readonly ["applicableSpecs", "GET", "/api/v1/taxonomy/categories/{key}/specs", "get_applicable_specs"], readonly ["glossary", "GET", "/api/v1/glossary/{term}", "explain_material_term"], readonly ["brands", "GET", "/api/v1/brands", "list_brands"], readonly ["brand", "GET", "/api/v1/brands/{brandSlug}", "get_brand_profile"], readonly ["brandColourDeck", "GET", "/api/v1/brands/{brandSlug}/colours", "brand_colour_deck"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["sourceMaterialComments", "GET", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "list_source_material_comments"], readonly ["sourceMaterialComments", "POST", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "post_source_material_comment"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["listRuns", "GET", "/api/v1/runs", "list_public_runs"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["streamRunEvents", "GET", "/api/v1/runs/{runId}/events", "list_public_run_events"], readonly ["resumeRun", "POST", "/api/v1/runs/{runId}/resume", "resume_public_run"], readonly ["cancelRun", "POST", "/api/v1/runs/{runId}/cancel", "cancel_public_run"], readonly ["varyScheme", "POST", "/api/v1/schemes/vary", "vary_schemes"], readonly ["slotPools", "POST", "/api/v1/schemes/pools", "slot_pools"], readonly ["createProject", "POST", "/api/v1/projects", "create_box_project"], readonly ["listProjects", "GET", "/api/v1/projects", "list_box_projects"], readonly ["getProject", "GET", "/api/v1/projects/{projectId}", "get_box_project"], readonly ["appendProjectEvent", "POST", "/api/v1/projects/{projectId}/events", "append_box_project_event"], readonly ["demandBatch", "POST", "/api/v1/demand/batch", "get_demand_batch"], readonly ["codeAdvisory", "POST", "/api/v1/codes/advisory", "resolveCodeAdvisory"], readonly ["codes.context", "GET", "/api/v1/codes/context", "list_code_context"], readonly ["codes.requirements", "GET", "/api/v1/codes/requirements", "list_code_requirements"]];
|
|
2
|
+
declare const OPERATION_DEFINITIONS: readonly [readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["planQuery", "POST", "/api/v1/search/plan", "plan_material_query"], readonly ["searchGet", "GET", "/api/v1/search", "search_materials_get"], readonly ["match", "POST", "/api/v1/search/match", "match_material"], readonly ["byColour", "GET", "/api/v1/search/colours", "search_by_colour"], readonly ["material", "GET", "/api/v1/materials/{variantId}", "get_material"], readonly ["siblings", "GET", "/api/v1/materials/{variantId}/siblings", "get_material_siblings"], readonly ["materialBlocks", "GET", "/api/v1/materials/{variantId}/blocks", "list_material_blocks"], readonly ["materialAssets", "GET", "/api/v1/materials/{variantId}/assets", "list_material_assets"], readonly ["exploration.operators", "GET", "/api/v1/materials/{variantId}/operators", "list_material_operators"], readonly ["exploration.candidates", "POST", "/api/v1/materials/{variantId}/candidates", "read_material_candidates"], readonly ["schema", "GET", "/api/v1/schema/blocks", "list_schema_blocks"], readonly ["schema", "GET", "/api/v1/schema/attributes", "list_schema_attributes"], readonly ["schema", "GET", "/api/v1/schema/verticals", "list_schema_verticals"], readonly ["schema", "GET", "/api/v1/schema/product-types", "list_schema_product_types"], readonly ["schema", "GET", "/api/v1/schema/units", "list_schema_units"], readonly ["schema", "GET", "/api/v1/schema/value-dictionaries", "list_schema_value_dictionaries"], readonly ["schema", "GET", "/api/v1/schema/source-requirements", "list_schema_source_requirements"], readonly ["schema", "GET", "/api/v1/schema/product-type-templates", "list_schema_product_type_templates"], readonly ["schema", "GET", "/api/v1/schema/mappings", "list_schema_mappings"], readonly ["pairings", "GET", "/api/v1/materials/{variantId}/pairings", "get_material_pairings"], readonly ["associatedFinishes", "GET", "/api/v1/associated/finishes", "list_associated_finishes"], readonly ["associatedProducts", "GET", "/api/v1/associated/products", "list_associated_products"], readonly ["materials", "POST", "/api/v1/materials/batch", "get_materials_batch_post"], readonly ["materialsCached", "GET", "/api/v1/materials/batch", "get_materials_batch_get"], readonly ["browse", "GET", "/api/v1/search/browse", "search_browse_by_colour"], readonly ["colourFacets", "GET", "/api/v1/colours/facets", "get_colour_facets"], readonly ["colours.profiles", "GET", "/api/v1/colours/profiles", "get_category_colour_profiles"], readonly ["colours.stats", "GET", "/api/v1/colours/stats", "get_colour_stats"], readonly ["resolveColour", "GET", "/api/v1/search/colours/resolve", "resolve_colour_name"], readonly ["suggestColour", "GET", "/api/v1/search/colours/suggest", "suggest_colour_names"], readonly ["suggest", "GET", "/api/v1/search/suggest", "search_suggest"], readonly ["resolveAttribute", "GET", "/api/v1/catalog/attributes/resolve", "catalog_resolve_attribute"], readonly ["resolveAttributeValue", "GET", "/api/v1/catalog/attributes/{attributeId}/values/resolve", "catalog_resolve_value"], readonly ["searchVocabulary", "GET", "/api/v1/catalog/vocabulary/search", "catalog_vocabulary_search"], readonly ["resolve", "POST", "/api/v1/resolve", "resolve_phrases"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["whoami", "GET", "/api/v1/whoami", "whoami"], readonly ["coordinate", "POST", "/api/v1/pairings/coordinate", "coordinate_materials"], readonly ["demand", "GET", "/api/v1/demand", "get_material_demand"], readonly ["colours.palette", "POST", "/api/v1/search/palettes", "search_by_palette"], readonly ["extractPalette", "POST", "/api/v1/search/palettes/extract", "extract_image_palette"], readonly ["colours.gradient", "POST", "/api/v1/search/gradients", "search_by_gradient"], readonly ["specificationFacets", "GET", "/api/v1/catalog/specifications/facets", "list_spec_facets"], readonly ["attributeValues", "GET", "/api/v1/catalog/attributes/{attributeId}/values", "list_attribute_values"], readonly ["applicableSpecs", "GET", "/api/v1/taxonomy/categories/{key}/specs", "get_applicable_specs"], readonly ["glossary", "GET", "/api/v1/glossary/{term}", "explain_material_term"], readonly ["brands", "GET", "/api/v1/brands", "list_brands"], readonly ["brand", "GET", "/api/v1/brands/{brandSlug}", "get_brand_profile"], readonly ["brandColourDeck", "GET", "/api/v1/brands/{brandSlug}/colours", "brand_colour_deck"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["sourceMaterialComments", "GET", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "list_source_material_comments"], readonly ["sourceMaterialComments", "POST", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "post_source_material_comment"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["listRuns", "GET", "/api/v1/runs", "list_public_runs"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["streamRunEvents", "GET", "/api/v1/runs/{runId}/events", "list_public_run_events"], readonly ["resumeRun", "POST", "/api/v1/runs/{runId}/resume", "resume_public_run"], readonly ["cancelRun", "POST", "/api/v1/runs/{runId}/cancel", "cancel_public_run"], readonly ["varyScheme", "POST", "/api/v1/schemes/vary", "vary_schemes"], readonly ["slotPools", "POST", "/api/v1/schemes/pools", "slot_pools"], readonly ["createProject", "POST", "/api/v1/projects", "create_box_project"], readonly ["listProjects", "GET", "/api/v1/projects", "list_box_projects"], readonly ["getProject", "GET", "/api/v1/projects/{projectId}", "get_box_project"], readonly ["appendProjectEvent", "POST", "/api/v1/projects/{projectId}/events", "append_box_project_event"], readonly ["demandBatch", "POST", "/api/v1/demand/batch", "get_demand_batch"], readonly ["codeAdvisory", "POST", "/api/v1/codes/advisory", "resolveCodeAdvisory"], readonly ["codes.context", "GET", "/api/v1/codes/context", "list_code_context"], readonly ["codes.requirements", "GET", "/api/v1/codes/requirements", "list_code_requirements"]];
|
|
3
3
|
export type SdkOperationId = (typeof OPERATION_DEFINITIONS)[number][3];
|
|
4
4
|
export type SdkOperation = Readonly<{
|
|
5
5
|
clientMethod: (typeof OPERATION_DEFINITIONS)[number][0];
|
package/operations.js
CHANGED
|
@@ -2,6 +2,7 @@ import { MaterialGraphError } from "./errors.js";
|
|
|
2
2
|
export const MATERIALGRAPH_API_VERSION = "0.7.0";
|
|
3
3
|
const OPERATION_DEFINITIONS = [
|
|
4
4
|
["search", "POST", "/api/v1/search", "search_materials_post"],
|
|
5
|
+
["planQuery", "POST", "/api/v1/search/plan", "plan_material_query"],
|
|
5
6
|
["searchGet", "GET", "/api/v1/search", "search_materials_get"],
|
|
6
7
|
["match", "POST", "/api/v1/search/match", "match_material"],
|
|
7
8
|
["byColour", "GET", "/api/v1/search/colours", "search_by_colour"],
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OpenEnum } from "./types.js";
|
|
2
|
+
export type QuerySenseKind = OpenEnum<"brand" | "product" | "paint_identity" | "category" | "material" | "colour" | "attribute" | "attribute_value" | "certification">;
|
|
3
|
+
export interface QuerySense {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: QuerySenseKind;
|
|
6
|
+
label: string;
|
|
7
|
+
outcome: OpenEnum<"applied" | "ambiguous" | "recognized" | "refused">;
|
|
8
|
+
effect: OpenEnum<"restrict" | "prioritize" | "rerank" | "offer" | "none">;
|
|
9
|
+
confidence: number;
|
|
10
|
+
evidence: {
|
|
11
|
+
source: string;
|
|
12
|
+
matchedText: string;
|
|
13
|
+
span?: {
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
entity?: {
|
|
19
|
+
brandSlug?: string;
|
|
20
|
+
productId?: string;
|
|
21
|
+
variantId?: string;
|
|
22
|
+
materialBankId?: string;
|
|
23
|
+
productTypeId?: string;
|
|
24
|
+
paintCode?: string;
|
|
25
|
+
hex?: string;
|
|
26
|
+
};
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Inspectable interpretation of the raw query by ordinary MaterialGraph search. */
|
|
30
|
+
export interface QueryUnderstanding {
|
|
31
|
+
version: OpenEnum<"1">;
|
|
32
|
+
rawQuery: string;
|
|
33
|
+
normalisedQuery: string;
|
|
34
|
+
residualText: string;
|
|
35
|
+
status: OpenEnum<"resolved" | "ambiguous" | "partial" | "unresolved">;
|
|
36
|
+
mode: OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse">;
|
|
37
|
+
senses: QuerySense[];
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -28,14 +28,21 @@ export interface SearchFilterTrust {
|
|
|
28
28
|
minConfidence?: number;
|
|
29
29
|
/**
|
|
30
30
|
* Demand an EXACT source basis (closed enum — an unknown basis is rejected).
|
|
31
|
-
*
|
|
32
|
-
* - `tested` — a measured result. The strongest claim available.
|
|
31
|
+
* Strongest to weakest as evidence:
|
|
33
32
|
* - `declared` — the manufacturer stated it. Attributable, not independent.
|
|
34
33
|
* - `catalog_feed` — it arrived in a catalog feed. Trusted at ingest, unreviewed.
|
|
35
34
|
* - `derived` — MaterialGraph inferred it. Ours, not the manufacturer's — never
|
|
36
35
|
* present a `derived` value as something the brand claims.
|
|
36
|
+
*
|
|
37
|
+
* There is NO `tested` basis. This docblock advertised one as "the strongest
|
|
38
|
+
* claim available" through 0.12.0, and the union listed `declared` twice in
|
|
39
|
+
* its place — so a caller reading the documentation would write a value the
|
|
40
|
+
* server rejects with a 400, and the type would not stop them because the
|
|
41
|
+
* fourth arm was a duplicate rather than the value described.
|
|
42
|
+
* `packages/schema/src/source-basis.test.ts` asserts `tested` parses false;
|
|
43
|
+
* its absence is deliberate, not an omission.
|
|
37
44
|
*/
|
|
38
|
-
sourceBasis?: "catalog_feed" | "declared" | "derived"
|
|
45
|
+
sourceBasis?: "catalog_feed" | "declared" | "derived";
|
|
39
46
|
/** Fold the attribute's own registry confidence requirement in, server-side. */
|
|
40
47
|
meetsRequirement?: true;
|
|
41
48
|
}
|
|
@@ -49,6 +56,22 @@ export type SearchFilter = {
|
|
|
49
56
|
attributeId: string;
|
|
50
57
|
term: string;
|
|
51
58
|
trust?: SearchFilterTrust;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* TERM SET — the attribute must equal any ONE of these. OR within the entry;
|
|
62
|
+
* AND still holds between entries, so a filter rail composes the same way
|
|
63
|
+
* whatever it is filtering on.
|
|
64
|
+
*
|
|
65
|
+
* The server has accepted this since the term-set arm shipped, and a consumer
|
|
66
|
+
* has been sending it successfully on the wire — but it was missing from this
|
|
67
|
+
* union through 0.12.0, so multi-value filtering type-checked nowhere while
|
|
68
|
+
* working everywhere. Brand multi-select is the common case:
|
|
69
|
+
* `{attributeId: "brand", terms: ["kvadrat", "interface"]}`.
|
|
70
|
+
*/
|
|
71
|
+
| {
|
|
72
|
+
attributeId: string;
|
|
73
|
+
terms: string[];
|
|
74
|
+
trust?: SearchFilterTrust;
|
|
52
75
|
} | {
|
|
53
76
|
attributeId: string;
|
|
54
77
|
hasValue: true;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* repo's source-size gate; every name here is re-exported from `types.ts`, so
|
|
6
6
|
* no import path changes.
|
|
7
7
|
*/
|
|
8
|
+
import type { CorpusScope } from "./corpus-scope-types.js";
|
|
8
9
|
import type { Cursor, VariantId } from "./identifiers.js";
|
|
9
10
|
import type { SearchFilter } from "./search-refinement-types.js";
|
|
10
11
|
import type { ColourInput, OpenEnum, SearchImageInput } from "./types.js";
|
|
@@ -30,7 +31,41 @@ export interface SearchInput {
|
|
|
30
31
|
};
|
|
31
32
|
};
|
|
32
33
|
mood?: string;
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Re-order the final scored results. `relevance` (default) keeps the fused
|
|
36
|
+
* score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
|
|
37
|
+
* ramps; `name` (A→Z), `name_desc` (Z→A) and `recent` (newest first) are the
|
|
38
|
+
* catalogue orders — the same three `/search/browse` walks the corpus in, so
|
|
39
|
+
* one word means one order on both lanes.
|
|
40
|
+
*/
|
|
41
|
+
sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
|
|
42
|
+
/**
|
|
43
|
+
* Sort along ONE attribute's declared value ladder — a SEPARATE axis from
|
|
44
|
+
* `sort`, which stays the closed relevance / colour / catalogue vocabulary.
|
|
45
|
+
*
|
|
46
|
+
* `direction` defaults to `"asc"`. Sent together with `sort`, `orderBy` wins
|
|
47
|
+
* and the response `note` says so — a UI holding both controls can send both.
|
|
48
|
+
*
|
|
49
|
+
* Ordering is DECLARED, never inferred from how a value is spelled: an
|
|
50
|
+
* attribute with no stated ladder is a 400 carrying the orderable set in
|
|
51
|
+
* `orderable_attributes`, not a quiet fall back to relevance. A material whose
|
|
52
|
+
* value is off the ladder, or which carries no value at all, sorts LAST in
|
|
53
|
+
* both directions — it is still a real material, it just has no rung.
|
|
54
|
+
*/
|
|
55
|
+
orderBy?: {
|
|
56
|
+
attributeId: string;
|
|
57
|
+
direction?: OpenEnum<"asc" | "desc">;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Attribute ids whose resolved value should ride each result under
|
|
61
|
+
* `attributes` — so a page you filtered by `price_tier` can DISPLAY the tier
|
|
62
|
+
* without a second call per material. Up to 8.
|
|
63
|
+
*
|
|
64
|
+
* Three response states, all different: no `attributes` key means you did not
|
|
65
|
+
* ask, a key with `null` means nothing is on record for that material, and
|
|
66
|
+
* anything else is the value. An unknown attribute id is a 400 naming it.
|
|
67
|
+
*/
|
|
68
|
+
returnAttributes?: string[];
|
|
34
69
|
/**
|
|
35
70
|
* Seed a visual search from an existing variant's image (`variantId`), from a
|
|
36
71
|
* Material Bank SKU (`materialBankId`), or from a pre-computed image vector
|
|
@@ -99,6 +134,11 @@ export interface SearchInput {
|
|
|
99
134
|
* `facets.absent`. Absent = no `facets` in the response, no counting cost.
|
|
100
135
|
*/
|
|
101
136
|
facetCounts?: string[];
|
|
137
|
+
/**
|
|
138
|
+
* Opaque five-minute receipt from a prior pool-derived faceted search.
|
|
139
|
+
* Resubmitting it may only narrow that exact finalized pool.
|
|
140
|
+
*/
|
|
141
|
+
refinementToken?: string;
|
|
102
142
|
/** Only return materials with at least one catalog image. Default false (full catalog). */
|
|
103
143
|
hasImages?: boolean;
|
|
104
144
|
/**
|
|
@@ -126,10 +166,17 @@ export interface SearchInput {
|
|
|
126
166
|
* **ABSENT means the WHOLE GRAPH** — every material MaterialGraph knows,
|
|
127
167
|
* orderable or not. Absence is never a narrower default, so a caller that
|
|
128
168
|
* says nothing gets materials it may not be able to buy.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
169
|
+
* Three scopes gate on CHANNEL membership, which is what "can someone buy
|
|
170
|
+
* this" means: `"materialBankNaOrderable"` (Material Bank North America,
|
|
171
|
+
* ~277,000 materials), `"materialBankEuOrderable"` (Material Bank Europe,
|
|
172
|
+
* ~65,000) and `"designShopOrderable"` (DesignShop, ~34,000, with the
|
|
173
|
+
* products DesignShop hides from browse excluded).
|
|
174
|
+
*
|
|
175
|
+
* `"materialBankOrderable"` is NOT one of them, despite the name. It selects
|
|
176
|
+
* materials carrying a Material Bank identity — a PROVENANCE signal, not a
|
|
177
|
+
* storefront — and admits about 98% of the graph. Use it to exclude the
|
|
178
|
+
* crawl-sourced brands that were never in the Material Bank feed; if you mean
|
|
179
|
+
* orderable, use one of the three above.
|
|
133
180
|
*
|
|
134
181
|
* A scope answers what is BUYABLE. It never answers what is SUITABLE: scope
|
|
135
182
|
* on channel, then refine on sector. A trade-only material can be right for a
|
|
@@ -142,7 +189,7 @@ export interface SearchInput {
|
|
|
142
189
|
* shortens the page exactly when the query drifts toward the corpus beyond
|
|
143
190
|
* the Material Bank feed, which is unpredictable from outside.
|
|
144
191
|
*/
|
|
145
|
-
corpusScope?:
|
|
192
|
+
corpusScope?: CorpusScope;
|
|
146
193
|
/**
|
|
147
194
|
* OPS/AUDIT escape hatch — do NOT use for normal search. Default behaviour
|
|
148
195
|
* (absent or false) excludes sampling-artifact products: multi-swatch sample
|
|
@@ -156,7 +203,7 @@ export interface SearchGetParams {
|
|
|
156
203
|
colours?: string;
|
|
157
204
|
logic?: OpenEnum<"all" | "any">;
|
|
158
205
|
mood?: string;
|
|
159
|
-
sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
|
|
206
|
+
sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
|
|
160
207
|
tol?: OpenEnum<"tight" | "normal" | "loose">;
|
|
161
208
|
/** Seed the search from this variant's image. Opaque — pass it through verbatim. */
|
|
162
209
|
variantId?: VariantId;
|
|
@@ -169,4 +216,12 @@ export interface SearchGetParams {
|
|
|
169
216
|
hasImages?: OpenEnum<"true">;
|
|
170
217
|
/** Result projection — `"full"` (default) or the lean `"card"` grid form. */
|
|
171
218
|
view?: OpenEnum<"full" | "card">;
|
|
219
|
+
/**
|
|
220
|
+
* One JSON OBJECT, e.g. `{"attributeId":"price_tier","direction":"desc"}` —
|
|
221
|
+
* the `orderBy` field of the POST body, JSON-encoded because a query string
|
|
222
|
+
* has no nested values. Same spelling as `filters=` on `/search/browse`.
|
|
223
|
+
*/
|
|
224
|
+
orderBy?: string;
|
|
225
|
+
/** Comma-separated attribute ids — the `returnAttributes` list. */
|
|
226
|
+
returnAttributes?: string;
|
|
172
227
|
}
|
package/types.d.ts
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import type { BySpecVerdict } from "./by-spec-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hand-mirrored wire types for the MaterialGraph public API (spec 0.5.0).
|
|
4
|
+
*
|
|
5
|
+
* These interfaces intentionally have NO runtime dependency on any
|
|
6
|
+
* `@materialgraph/*` package — they are a standalone, publishable mirror of the
|
|
7
|
+
* shapes in `apps/web/public/openapi.yaml` and the captured fixtures under
|
|
8
|
+
* `docs/surface/fixtures/v0.5.0/`. The `contract-parity` test re-checks them
|
|
9
|
+
* against the real Zod schemas so the mirror cannot silently drift.
|
|
10
|
+
* TOLERANT-PARSING CONTRACT: the public surface declares open response enums —
|
|
11
|
+
* "clients must tolerate unknown enum values and unknown fields". Every union
|
|
12
|
+
* that mirrors a server enum is therefore widened with `(string & {})` so a new
|
|
13
|
+
* server value still type-checks, and objects that may grow fields are read
|
|
14
|
+
* structurally (unknown keys pass through untouched — the client never strips).
|
|
15
|
+
*
|
|
16
|
+
* READING CONTRACT — the three things callers get wrong, stated once here and
|
|
17
|
+
* repeated on the types where each one bites:
|
|
18
|
+
* 1. **Ids are opaque.** Round-trip them verbatim; never parse or construct
|
|
19
|
+
* one. See `./identifiers.ts` for which strings are opaque tokens and which
|
|
20
|
+
* are canonical vocabulary you discover.
|
|
21
|
+
* 2. **Absent is not false.** An optional field that is missing means UNKNOWN,
|
|
22
|
+
* not "known to be absent in reality" — most dangerously for booleans.
|
|
23
|
+
* Values carry `confidence` and `sourceBasis`; report them as claims.
|
|
24
|
+
* 3. **A page is not a corpus.** `results.length` is this page.
|
|
25
|
+
* {@link CountedTotal} — with the `basis` it states — is the claim about the
|
|
26
|
+
* match set. Follow the cursor for the rest, and never report a page size as
|
|
27
|
+
* a total.
|
|
28
|
+
*/
|
|
29
|
+
import type { CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
2
30
|
import type { Cursor, ProductId, VariantId } from "./identifiers.js";
|
|
3
31
|
import type { MaterialDetail, MaterialTile } from "./material-types.js";
|
|
32
|
+
import type { QueryUnderstanding } from "./query-understanding-types.js";
|
|
4
33
|
import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
|
|
5
34
|
/** A widened string-literal union: known members autocomplete, unknown values still type-check. */
|
|
6
35
|
export type OpenEnum<T extends string> = T | (string & {});
|
|
@@ -226,6 +255,18 @@ export interface SearchResultItem {
|
|
|
226
255
|
* shared CandidateVerdict mirror; see {@link MaterialVerdict}.
|
|
227
256
|
*/
|
|
228
257
|
verdict?: BySpecVerdict;
|
|
258
|
+
/**
|
|
259
|
+
* Resolved values for the attribute ids sent in `returnAttributes`, keyed by
|
|
260
|
+
* attribute id. ABSENT entirely when `returnAttributes` was not sent.
|
|
261
|
+
*
|
|
262
|
+
* Three states, all different, and reading them as two is the mistake this
|
|
263
|
+
* shape exists to prevent: no `attributes` key means you did not ask; a key
|
|
264
|
+
* whose value is `null` means this material carries no accepted value for that
|
|
265
|
+
* attribute; anything else is what the graph holds. Values resolve with the
|
|
266
|
+
* normal variant-over-product precedence, and an array is a multi-valued
|
|
267
|
+
* attribute rather than a list of candidates.
|
|
268
|
+
*/
|
|
269
|
+
attributes?: Record<string, string | string[] | null>;
|
|
229
270
|
}
|
|
230
271
|
/**
|
|
231
272
|
* The lean `view:"card"` result item — identity + name + thumbnail + score.
|
|
@@ -246,6 +287,18 @@ export interface SearchCardItem {
|
|
|
246
287
|
variantName: string | null;
|
|
247
288
|
imageUrl: string | null;
|
|
248
289
|
score: number;
|
|
290
|
+
/**
|
|
291
|
+
* Resolved values for the attribute ids sent in `returnAttributes`, keyed by
|
|
292
|
+
* attribute id. ABSENT entirely when `returnAttributes` was not sent.
|
|
293
|
+
*
|
|
294
|
+
* Three states, all different, and reading them as two is the mistake this
|
|
295
|
+
* shape exists to prevent: no `attributes` key means you did not ask; a key
|
|
296
|
+
* whose value is `null` means this material carries no accepted value for that
|
|
297
|
+
* attribute; anything else is what the graph holds. Values resolve with the
|
|
298
|
+
* normal variant-over-product precedence, and an array is a multi-valued
|
|
299
|
+
* attribute rather than a list of candidates.
|
|
300
|
+
*/
|
|
301
|
+
attributes?: Record<string, string | string[] | null>;
|
|
249
302
|
}
|
|
250
303
|
/**
|
|
251
304
|
* A feed total with its honesty basis — THE CLAIM ABOUT THE MATCH SET, which is
|
|
@@ -326,8 +379,16 @@ export interface SearchFacets {
|
|
|
326
379
|
absent: SearchAbsentFacet[];
|
|
327
380
|
counts: Record<string, SearchFacetCount>;
|
|
328
381
|
}
|
|
382
|
+
/** Inspectable metadata for an opaque, principal-bound search refinement receipt. */
|
|
383
|
+
export interface SearchRefinementReceipt {
|
|
384
|
+
token: string;
|
|
385
|
+
expiresAt: string;
|
|
386
|
+
poolSize: number;
|
|
387
|
+
schemaVersion: string;
|
|
388
|
+
projectionVersion: number;
|
|
389
|
+
}
|
|
329
390
|
/** The single candidate source a query resolved to. */
|
|
330
|
-
export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical">;
|
|
391
|
+
export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse">;
|
|
331
392
|
/** Rerank-term report (absent in colour mode — there colour IS the source). */
|
|
332
393
|
export interface SearchRerankReport {
|
|
333
394
|
colour?: OpenEnum<"ok" | "failed" | "skipped">;
|
|
@@ -373,6 +434,8 @@ export interface SearchResponse {
|
|
|
373
434
|
droppedColours?: string[];
|
|
374
435
|
/** Per-query facet counts — present ONLY when the request sent `facetCounts`. */
|
|
375
436
|
facets?: SearchFacets;
|
|
437
|
+
/** Receipt that narrows the exact finalized pool behind lower-bound facets. */
|
|
438
|
+
refinement?: SearchRefinementReceipt;
|
|
376
439
|
note?: string;
|
|
377
440
|
timings?: Record<string, number>;
|
|
378
441
|
/** A detected mood term overridden by a stronger colour reading. */
|
|
@@ -382,17 +445,15 @@ export interface SearchResponse {
|
|
|
382
445
|
* offers the other as a complete executable request.
|
|
383
446
|
*/
|
|
384
447
|
disambiguation?: SearchDisambiguation;
|
|
448
|
+
/** Structured receipt of the standard query-understanding decisions. */
|
|
449
|
+
understanding?: QueryUnderstanding;
|
|
385
450
|
/**
|
|
386
451
|
* The corpus this page AND its `counts` were drawn from — present ONLY when
|
|
387
452
|
* the request narrowed it. ABSENT means the whole graph: every material
|
|
388
453
|
* MaterialGraph knows, orderable or not. Read it alongside `counts`: a short
|
|
389
454
|
* page under a scope is a small SCOPED corpus, not a truncated one.
|
|
390
455
|
*/
|
|
391
|
-
corpusScope?:
|
|
392
|
-
scope: OpenEnum<"all" | "materialBankOrderable">;
|
|
393
|
-
/** What the scope admits, in one sentence, for a human reader. */
|
|
394
|
-
description: string;
|
|
395
|
-
};
|
|
456
|
+
corpusScope?: CorpusScopeReceipt;
|
|
396
457
|
/** Echo of the resolved query input (raw image bytes/vector are never echoed). */
|
|
397
458
|
query?: Record<string, unknown>;
|
|
398
459
|
/** A `getVariantBrief` echo of the seed variant, present only in `variantId` seed mode. */
|