@instruments/materialgraph 0.12.1 → 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 CHANGED
@@ -15,7 +15,7 @@ import type { ByColourSwatch } from "./by-colour-types.js";
15
15
  */
16
16
  import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
17
17
  import type { Cursor, ProductId, VariantId } from "./identifiers.js";
18
- import type { CountedTotal, OpenEnum } from "./types.js";
18
+ import type { CountedTotal, OpenEnum, SearchFacets } from "./types.js";
19
19
  /** One item from the pure colour-scalar browse. */
20
20
  export interface BrowseResultItem {
21
21
  /** Opaque — round-trip verbatim. */
@@ -44,9 +44,20 @@ export interface BrowseResultItem {
44
44
  * from product imagery, not from the manufacturer.
45
45
  */
46
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>;
47
54
  }
48
- /** The browse orders. `name` (A→Z) is the default; `recent` is newest first. */
49
- export type BrowseSort = OpenEnum<"name" | "recent" | "light" | "dark" | "vivid" | "muted" | "colourful">;
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">;
50
61
  /**
51
62
  * The browse feed's total — how many materials the browse DESCRIBES, which is
52
63
  * never `results.length`.
@@ -79,11 +90,37 @@ export interface BrowseResponse {
79
90
  * to label its own listing.
80
91
  */
81
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;
82
107
  /**
83
108
  * The total, present ONLY when `count: true` was passed. Absent means it was
84
109
  * not asked for — never that the set is empty.
85
110
  */
86
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;
87
124
  /**
88
125
  * The corpus this page was drawn from — present ONLY when the request
89
126
  * narrowed it. ABSENT means the whole graph: every material MaterialGraph
@@ -133,6 +170,30 @@ export interface BrowseParams {
133
170
  * brand's fan deck. An unknown brand is a 400 naming it, never an empty page.
134
171
  */
135
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[];
136
197
  /**
137
198
  * Which corpus to walk. ABSENT means the whole graph — every material
138
199
  * MaterialGraph knows, orderable or not. `materialBankOrderable` restricts to
@@ -141,4 +202,24 @@ export interface BrowseParams {
141
202
  * row exists.
142
203
  */
143
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[];
144
225
  }
@@ -6,8 +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 { CorpusScopeReceipt } from "./corpus-scope-types.js";
9
+ import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
10
10
  import type { VariantId } from "./identifiers.js";
11
+ import type { QueryUnderstanding } from "./query-understanding-types.js";
11
12
  import type { OklabTriple, OpenEnum, PaletteSwatch } from "./types.js";
12
13
  /** Coarse group + fine type of a colour match's primary product type. */
13
14
  export interface MatchCategory {
@@ -27,6 +28,12 @@ export interface ByColourMatch {
27
28
  variantId: VariantId;
28
29
  brand: string;
29
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;
30
37
  color: string | null;
31
38
  variantName: string | null;
32
39
  imageUrl: string | null;
@@ -148,6 +155,8 @@ export interface ColourLaneReceipt {
148
155
  /** The `search/colours` response. */
149
156
  export interface ByColourResponse {
150
157
  query: {
158
+ /** Raw query provenance, when supplied. */
159
+ q?: string;
151
160
  colours: string[];
152
161
  categories?: string[];
153
162
  /**
@@ -188,9 +197,13 @@ export interface ByColourResponse {
188
197
  * retrieval rather than over the page it produced.
189
198
  */
190
199
  corpusScope?: CorpusScopeReceipt;
200
+ /** Shared standard-search query-understanding receipt. */
201
+ understanding?: QueryUnderstanding;
191
202
  }
192
203
  /** Params for `search/colours`. */
193
204
  export interface ByColourParams {
205
+ /** Raw material-domain query provenance for shared query understanding. */
206
+ q?: string;
194
207
  /** A single hex (with or without `#`). */
195
208
  hex?: string;
196
209
  /** A palette — repeated as one `colour` query entry each. */
@@ -232,5 +245,7 @@ export interface ByColourParams {
232
245
  * drop.
233
246
  */
234
247
  family?: string;
248
+ /** Narrow retrieval inside the server query; absent means the whole graph. */
249
+ corpusScope?: CorpusScope;
235
250
  limit?: number;
236
251
  }
@@ -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 (POST `/api/v1/search`). Layer a hard
131
- * specification gate onto a ranked (colour / visual / lexical / seed) search
132
- * via `input.filters` — each entry is a typed {@link SearchFilter} criterion:
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 (POST `/api/v1/search`). Layer a hard
161
- * specification gate onto a ranked (colour / visual / lexical / seed) search
162
- * via `input.filters` — each entry is a typed {@link SearchFilter} criterion:
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
  }
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
@@ -89,7 +89,7 @@ interface TransportErrorInput extends MaterialGraphRequestMetadata {
89
89
  * member to a problem response means adding it here too, and
90
90
  * `problem-extension-parity.test.ts` fails when the two disagree.
91
91
  */
92
- export declare const MATERIALGRAPH_PROBLEM_EXTENSIONS: readonly ["content_length", "did_you_mean", "empty_brands", "limit_bytes", "missing_env", "populated_alternatives", "unknown_attributes", "unknown_brands", "valid_group_keys", "valid_type_keys"];
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"];
93
93
  /** The one error class thrown by all SDK transport and HTTP failures. */
94
94
  export declare class MaterialGraphError extends Error {
95
95
  readonly kind: MaterialGraphErrorKind;
package/errors.js CHANGED
@@ -28,9 +28,12 @@ export const MATERIALGRAPH_PROBLEM_EXTENSIONS = [
28
28
  "empty_brands",
29
29
  "limit_bytes",
30
30
  "missing_env",
31
+ "orderable_attributes",
31
32
  "populated_alternatives",
32
33
  "unknown_attributes",
33
34
  "unknown_brands",
35
+ "unknown_fields",
36
+ "unorderable_attribute",
34
37
  "valid_group_keys",
35
38
  "valid_type_keys",
36
39
  ];
package/index.d.ts CHANGED
@@ -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";
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -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 {};
@@ -31,7 +31,41 @@ export interface SearchInput {
31
31
  };
32
32
  };
33
33
  mood?: string;
34
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
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[];
35
69
  /**
36
70
  * Seed a visual search from an existing variant's image (`variantId`), from a
37
71
  * Material Bank SKU (`materialBankId`), or from a pre-computed image vector
@@ -100,6 +134,11 @@ export interface SearchInput {
100
134
  * `facets.absent`. Absent = no `facets` in the response, no counting cost.
101
135
  */
102
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;
103
142
  /** Only return materials with at least one catalog image. Default false (full catalog). */
104
143
  hasImages?: boolean;
105
144
  /**
@@ -164,7 +203,7 @@ export interface SearchGetParams {
164
203
  colours?: string;
165
204
  logic?: OpenEnum<"all" | "any">;
166
205
  mood?: string;
167
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
206
+ sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
168
207
  tol?: OpenEnum<"tight" | "normal" | "loose">;
169
208
  /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
170
209
  variantId?: VariantId;
@@ -177,4 +216,12 @@ export interface SearchGetParams {
177
216
  hasImages?: OpenEnum<"true">;
178
217
  /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
179
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;
180
227
  }
package/types.d.ts CHANGED
@@ -29,6 +29,7 @@ import type { BySpecVerdict } from "./by-spec-types.js";
29
29
  import type { CorpusScopeReceipt } from "./corpus-scope-types.js";
30
30
  import type { Cursor, ProductId, VariantId } from "./identifiers.js";
31
31
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
32
+ import type { QueryUnderstanding } from "./query-understanding-types.js";
32
33
  import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
33
34
  /** A widened string-literal union: known members autocomplete, unknown values still type-check. */
34
35
  export type OpenEnum<T extends string> = T | (string & {});
@@ -254,6 +255,18 @@ export interface SearchResultItem {
254
255
  * shared CandidateVerdict mirror; see {@link MaterialVerdict}.
255
256
  */
256
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>;
257
270
  }
258
271
  /**
259
272
  * The lean `view:"card"` result item — identity + name + thumbnail + score.
@@ -274,6 +287,18 @@ export interface SearchCardItem {
274
287
  variantName: string | null;
275
288
  imageUrl: string | null;
276
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>;
277
302
  }
278
303
  /**
279
304
  * A feed total with its honesty basis — THE CLAIM ABOUT THE MATCH SET, which is
@@ -354,8 +379,16 @@ export interface SearchFacets {
354
379
  absent: SearchAbsentFacet[];
355
380
  counts: Record<string, SearchFacetCount>;
356
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
+ }
357
390
  /** The single candidate source a query resolved to. */
358
- export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical">;
391
+ export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse">;
359
392
  /** Rerank-term report (absent in colour mode — there colour IS the source). */
360
393
  export interface SearchRerankReport {
361
394
  colour?: OpenEnum<"ok" | "failed" | "skipped">;
@@ -401,6 +434,8 @@ export interface SearchResponse {
401
434
  droppedColours?: string[];
402
435
  /** Per-query facet counts — present ONLY when the request sent `facetCounts`. */
403
436
  facets?: SearchFacets;
437
+ /** Receipt that narrows the exact finalized pool behind lower-bound facets. */
438
+ refinement?: SearchRefinementReceipt;
404
439
  note?: string;
405
440
  timings?: Record<string, number>;
406
441
  /** A detected mood term overridden by a stronger colour reading. */
@@ -410,6 +445,8 @@ export interface SearchResponse {
410
445
  * offers the other as a complete executable request.
411
446
  */
412
447
  disambiguation?: SearchDisambiguation;
448
+ /** Structured receipt of the standard query-understanding decisions. */
449
+ understanding?: QueryUnderstanding;
413
450
  /**
414
451
  * The corpus this page AND its `counts` were drawn from — present ONLY when
415
452
  * the request narrowed it. ABSENT means the whole graph: every material