@instruments/materialgraph 0.17.0 → 0.18.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.
@@ -90,3 +90,64 @@ export interface AssociatedOptions {
90
90
  */
91
91
  cursor?: Cursor;
92
92
  }
93
+ export type ApplicabilitySubject = {
94
+ kind: "product";
95
+ id: string;
96
+ } | {
97
+ kind: "variant";
98
+ id: VariantId;
99
+ };
100
+ export type ApplicabilityCoverageState = "available" | "partial" | "none_declared" | "not_ingested" | "source_not_applicable" | "unavailable";
101
+ export interface ApplicabilityCoverage {
102
+ state: ApplicabilityCoverageState;
103
+ countKind: "exact" | "partial";
104
+ declaredCount: number | null;
105
+ sourceAuthority: string;
106
+ sourceLocator: string;
107
+ sourceRevision: string;
108
+ }
109
+ export interface ApplicabilityFact {
110
+ id: string;
111
+ subject: ApplicabilitySubject;
112
+ finish: {
113
+ kind: "variant";
114
+ id: VariantId;
115
+ };
116
+ relation: "applicable_finish";
117
+ evidenceScope: "manufacturer_declared";
118
+ declaringBrandId: string;
119
+ source: {
120
+ authority: string;
121
+ locator: string;
122
+ revision: string;
123
+ };
124
+ lifecycle: "active";
125
+ }
126
+ export interface ApplicabilitySourceEntry {
127
+ scope: {
128
+ sourceAuthority: string;
129
+ sourceLocator: string;
130
+ };
131
+ /** Null when facts exist but this direction has no coverage census. */
132
+ coverage: ApplicabilityCoverage | null;
133
+ /** Revisions carried by active facts, independent from the coverage revision. */
134
+ factRevisions: string[];
135
+ /** Active facts from this source only. */
136
+ facts: ApplicabilityFact[];
137
+ }
138
+ /** Source-grained response; sources are never merged or given hidden precedence. */
139
+ export interface ApplicabilityResponse {
140
+ kind: "manufacturer_declared_applicability";
141
+ requestedSubject: ApplicabilitySubject;
142
+ direction: "forward" | "reverse";
143
+ sources: ApplicabilitySourceEntry[];
144
+ }
145
+ export type ApplicabilityOptions = {
146
+ subjectKind: "product";
147
+ subjectId: string;
148
+ direction: "forward";
149
+ } | {
150
+ subjectKind: "variant";
151
+ subjectId: VariantId;
152
+ direction: "forward" | "reverse";
153
+ };
@@ -12,7 +12,7 @@
12
12
  * sections, never concatenate them, and never describe a pairing as
13
13
  * "available on".
14
14
  */
15
- import type { AssociatedOptions, DeclaredAssociationsResponse } from "./association-types.js";
15
+ import type { ApplicabilityOptions, ApplicabilityResponse, AssociatedOptions, DeclaredAssociationsResponse } from "./association-types.js";
16
16
  import { HttpTransport } from "./http-transport.js";
17
17
  import type { VariantId } from "./identifiers.js";
18
18
  /**
@@ -49,3 +49,5 @@ export declare function declaredFinishes(transport: HttpTransport, variantId: Va
49
49
  * @param variantId The FINISH variant. Opaque — pass back exactly what you were given.
50
50
  */
51
51
  export declare function declaredProducts(transport: HttpTransport, variantId: VariantId, options?: AssociatedOptions, signal?: AbortSignal): Promise<DeclaredAssociationsResponse>;
52
+ /** Read manufacturer-declared applicability without collapsing Product/Variant grain. */
53
+ export declare function applicability(transport: HttpTransport, options: ApplicabilityOptions, signal?: AbortSignal): Promise<ApplicabilityResponse>;
@@ -38,3 +38,7 @@ export function declaredFinishes(transport, variantId, options, signal) {
38
38
  export function declaredProducts(transport, variantId, options, signal) {
39
39
  return transport.requestJson(resolveOperation("list_associated_products"), { query: { variantId, ...options }, signal });
40
40
  }
41
+ /** Read manufacturer-declared applicability without collapsing Product/Variant grain. */
42
+ export function applicability(transport, options, signal) {
43
+ return transport.requestJson(resolveOperation("get_applicability"), { query: options, signal });
44
+ }
@@ -1,4 +1,4 @@
1
- import type { CatalogDocumentList, CatalogProductReferenceList, CatalogueDocumentAccessResult, CatalogueReferenceBatchResponse } from "./catalogue-types.js";
1
+ import type { CatalogDocumentList, CatalogPageListResult, CatalogProductReferenceReadResult, CatalogueChannelCollectionsResponse, CatalogueCollectionsOptions, CatalogueDocumentAccessResult, CataloguePageRepresentationReadResult, CatalogueReferenceBatchResponse } from "./catalogue-types.js";
2
2
  import type { HttpTransport } from "./http-transport.js";
3
3
  import type { VariantId } from "./identifiers.js";
4
4
  /** Catalogue publication operations inherited by the public SDK client. */
@@ -13,10 +13,19 @@ export declare class CatalogueClient {
13
13
  catalogueReferences(input: {
14
14
  variantIds: VariantId[];
15
15
  }, signal?: AbortSignal): Promise<CatalogueReferenceBatchResponse>;
16
+ /** List product collections with governed catalogue-channel membership. */
17
+ catalogueCollections(options?: CatalogueCollectionsOptions, signal?: AbortSignal): Promise<CatalogueChannelCollectionsResponse>;
16
18
  /** List immutable catalog documents retained from verified source material. */
17
19
  catalogs(signal?: AbortSignal): Promise<CatalogDocumentList>;
20
+ /** List stable one-based page identities and their representation status. */
21
+ catalogPages(catalogId: string, options?: {
22
+ limit?: number;
23
+ offset?: number;
24
+ }, signal?: AbortSignal): Promise<CatalogPageListResult>;
25
+ /** Read a retained page representation or its exact missing-artifact receipt. */
26
+ catalogPageRepresentation(pageId: string, signal?: AbortSignal): Promise<CataloguePageRepresentationReadResult>;
18
27
  /** Read every verified product/page reference for one retained catalog. */
19
- catalogReferences(catalogId: string, signal?: AbortSignal): Promise<CatalogProductReferenceList>;
28
+ catalogReferences(catalogId: string, signal?: AbortSignal): Promise<CatalogProductReferenceReadResult>;
20
29
  /** Resolve browser-safe access to one immutable catalogue document. */
21
30
  catalogueDocumentAccess(documentId: string, signal?: AbortSignal): Promise<CatalogueDocumentAccessResult>;
22
31
  }
@@ -13,10 +13,29 @@ export class CatalogueClient {
13
13
  catalogueReferences(input, signal) {
14
14
  return this.#transport.requestJson(resolveOperation("get_catalogue_references_batch"), { body: input, signal });
15
15
  }
16
+ /** List product collections with governed catalogue-channel membership. */
17
+ catalogueCollections(options = {}, signal) {
18
+ return this.#transport.requestJson(resolveOperation("list_catalogue_channel_collections"), {
19
+ query: {
20
+ limit: options.limit,
21
+ offset: options.offset,
22
+ sampleLimit: options.sampleLimit,
23
+ },
24
+ signal,
25
+ });
26
+ }
16
27
  /** List immutable catalog documents retained from verified source material. */
17
28
  catalogs(signal) {
18
29
  return this.#transport.requestJson(resolveOperation("list_catalog_documents"), { signal });
19
30
  }
31
+ /** List stable one-based page identities and their representation status. */
32
+ catalogPages(catalogId, options = {}, signal) {
33
+ return this.#transport.requestJson(resolveOperation("list_catalog_pages"), { pathParams: { catalogId }, query: options, signal });
34
+ }
35
+ /** Read a retained page representation or its exact missing-artifact receipt. */
36
+ catalogPageRepresentation(pageId, signal) {
37
+ return this.#transport.requestJson(resolveOperation("get_catalog_page_representation"), { pathParams: { pageId }, signal });
38
+ }
20
39
  /** Read every verified product/page reference for one retained catalog. */
21
40
  catalogReferences(catalogId, signal) {
22
41
  return this.#transport.requestJson(resolveOperation("list_catalog_product_references"), { pathParams: { catalogId }, signal });
@@ -8,6 +8,24 @@ export interface CatalogueReference {
8
8
  x: number;
9
9
  y: number;
10
10
  };
11
+ productRegion?: {
12
+ coordinateSpace: "normalized_0_1000";
13
+ height: number;
14
+ kind: "product_region";
15
+ review: {
16
+ approvedBy: string;
17
+ confidence: {
18
+ evidenceBasis: "human_visual_review" | "model_assisted_visual_review";
19
+ level: "reviewed";
20
+ };
21
+ method: "human" | "model_assisted";
22
+ model?: string;
23
+ reviewedAt: string;
24
+ };
25
+ width: number;
26
+ x: number;
27
+ y: number;
28
+ };
11
29
  variantId: string;
12
30
  brand: {
13
31
  id: string;
@@ -43,7 +61,16 @@ export type CatalogueReferenceResult = {
43
61
  reference: CatalogueReference;
44
62
  } | {
45
63
  variantId: string;
46
- status: "no_verified_reference" | "unresolved_variant" | "unavailable" | "upstream_failure";
64
+ status: "no_verified_reference";
65
+ } | {
66
+ variantId: string;
67
+ status: "unresolved_variant";
68
+ } | {
69
+ variantId: string;
70
+ status: "unavailable";
71
+ } | {
72
+ variantId: string;
73
+ status: "upstream_failure";
47
74
  };
48
75
  export interface CatalogueReferenceBatchResponse {
49
76
  results: CatalogueReferenceResult[];
@@ -56,6 +83,50 @@ export interface CatalogueReferenceBatchResponse {
56
83
  upstreamFailure: number;
57
84
  };
58
85
  }
86
+ /** Bounds for the flat catalogue-channel collection shelf. */
87
+ export interface CatalogueCollectionsOptions {
88
+ limit?: number;
89
+ offset?: number;
90
+ sampleLimit?: number;
91
+ }
92
+ /** One representative product image carried by a catalogue collection. */
93
+ export interface CatalogueCollectionSampleProduct {
94
+ assetId: string;
95
+ imageUrl: string;
96
+ sourceImageUrl: string;
97
+ productId: string;
98
+ productName: string;
99
+ variantId: string;
100
+ variantName: string | null;
101
+ }
102
+ /** A product collection whose variants have governed catalogue membership. */
103
+ export interface CatalogueChannelCollection {
104
+ id: string;
105
+ slug: string;
106
+ name: string;
107
+ homepageUrl: string | null;
108
+ productCount: number;
109
+ variantCount: number;
110
+ sampleProducts: CatalogueCollectionSampleProduct[];
111
+ }
112
+ /** The flat, paginated catalogue-channel collection read model. */
113
+ export interface CatalogueChannelCollectionsResponse {
114
+ collections: CatalogueChannelCollection[];
115
+ channel: {
116
+ token: "catalogue";
117
+ basis: "governed_variant_membership";
118
+ };
119
+ counts: {
120
+ basis: "exact_current_channel_membership";
121
+ };
122
+ pagination: {
123
+ count: number;
124
+ total: number;
125
+ limit: number;
126
+ offset: number;
127
+ hasMore: boolean;
128
+ };
129
+ }
59
130
  export interface CatalogueDocumentAccess {
60
131
  documentId: string;
61
132
  contentType: "application/pdf";
@@ -69,9 +140,19 @@ export type CatalogueDocumentAccessResult = {
69
140
  access: CatalogueDocumentAccess;
70
141
  } | {
71
142
  documentId: string;
72
- status: "unresolved_document" | "unavailable" | "forbidden" | "upstream_failure";
143
+ status: "unresolved_document";
144
+ } | {
145
+ documentId: string;
146
+ status: "unavailable";
147
+ } | {
148
+ documentId: string;
149
+ status: "forbidden";
150
+ } | {
151
+ documentId: string;
152
+ status: "upstream_failure";
73
153
  };
74
154
  export interface CatalogDocument {
155
+ availability: "available";
75
156
  brand: {
76
157
  id: string;
77
158
  name: string;
@@ -82,21 +163,212 @@ export interface CatalogDocument {
82
163
  mediaType: "application/pdf";
83
164
  pageCount: number;
84
165
  };
166
+ edition: CatalogEdition;
85
167
  id: string;
86
168
  kind: "document";
87
169
  productStatus: "partial" | "not_processed";
170
+ receipt: CatalogAvailableReceipt;
88
171
  title: string;
89
172
  }
173
+ export interface CatalogEdition {
174
+ id: string;
175
+ label: string;
176
+ title: string;
177
+ }
178
+ export interface CatalogAvailableReceipt {
179
+ extraction: {
180
+ references: number;
181
+ status: "partial" | "not_processed";
182
+ };
183
+ failures: CatalogFailureReceipt[];
184
+ pages: {
185
+ documentPositionBase: 1;
186
+ pageCount: number;
187
+ };
188
+ render: {
189
+ failedPages: Array<{
190
+ documentPage: number;
191
+ reason: string;
192
+ }>;
193
+ renderedPages: number;
194
+ status: "complete" | "partial" | "not_recorded";
195
+ };
196
+ source: {
197
+ checksum: {
198
+ algorithm: "sha256";
199
+ value: string;
200
+ };
201
+ sourceBasis: "brand_supplied";
202
+ retainedUrl: string;
203
+ status: "retained";
204
+ };
205
+ }
206
+ export interface CatalogFailureReceipt {
207
+ count: number;
208
+ reason: string;
209
+ stage: "source" | "render" | "extraction";
210
+ }
211
+ export interface UnavailableCatalogDocument {
212
+ availability: "source_unavailable";
213
+ brand: {
214
+ id: string;
215
+ name: string;
216
+ };
217
+ document: null;
218
+ edition: CatalogEdition;
219
+ id: string;
220
+ kind: "document";
221
+ productStatus: "not_processed";
222
+ receipt: {
223
+ extraction: {
224
+ references: 0;
225
+ status: "blocked_source_unavailable";
226
+ };
227
+ failures: CatalogFailureReceipt[];
228
+ pages: {
229
+ documentPositionBase: 1;
230
+ pageCount: null;
231
+ };
232
+ render: {
233
+ failedPages: never[];
234
+ renderedPages: 0;
235
+ status: "blocked_source_unavailable";
236
+ };
237
+ source: {
238
+ reason: "retained_source_not_found";
239
+ status: "unavailable";
240
+ };
241
+ };
242
+ title: string;
243
+ }
244
+ export type CatalogPublication = CatalogDocument | UnavailableCatalogDocument;
90
245
  export interface CatalogDocumentList {
91
- catalogs: CatalogDocument[];
246
+ catalogs: CatalogPublication[];
92
247
  source: "verified-source-material-registry";
93
248
  }
94
249
  export interface CatalogProductReferenceList {
95
250
  catalog: CatalogDocument;
96
251
  coverage: {
252
+ documentPages: {
253
+ total: number;
254
+ withVerifiedReferences: number;
255
+ };
97
256
  pagesWithReferences: number;
257
+ precision: {
258
+ reason: "labelled_denominator_not_recorded";
259
+ status: "not_measured";
260
+ };
261
+ productCoverage: {
262
+ reason: "source_product_denominator_not_recorded";
263
+ status: "not_measured";
264
+ };
98
265
  references: number;
266
+ unmatched: {
267
+ reasons: "unmatched_source_products_not_recorded"[];
268
+ status: "not_measured";
269
+ };
99
270
  };
100
271
  references: CatalogueReference[];
101
272
  source: "verified-source-material-registry";
273
+ status: "available";
102
274
  }
275
+ export interface CatalogProductReferenceUnavailable {
276
+ catalog: UnavailableCatalogDocument;
277
+ reason: "retained_source_not_found";
278
+ status: "source_unavailable";
279
+ }
280
+ export type CatalogProductReferenceReadResult = CatalogProductReferenceList | CatalogProductReferenceUnavailable;
281
+ export interface CataloguePageRequiredArtifact {
282
+ mediaType: "image/webp";
283
+ version: "catalogue-page-webp-v1";
284
+ }
285
+ export interface CataloguePageAvailableArtifact extends Omit<CataloguePageRequiredArtifact, "version"> {
286
+ bucket: "materialgraph";
287
+ key: string;
288
+ url: string;
289
+ }
290
+ export type CataloguePage = {
291
+ catalogId: string;
292
+ documentId: string;
293
+ editionId: string;
294
+ id: string;
295
+ original: {
296
+ documentId: string;
297
+ documentPage: number;
298
+ };
299
+ page: {
300
+ documentPage: number;
301
+ printedPageLabel?: string;
302
+ };
303
+ representation: {
304
+ artifact: CataloguePageAvailableArtifact;
305
+ status: "available";
306
+ };
307
+ } | {
308
+ catalogId: string;
309
+ documentId: string;
310
+ editionId: string;
311
+ id: string;
312
+ original: {
313
+ documentId: string;
314
+ documentPage: number;
315
+ };
316
+ page: {
317
+ documentPage: number;
318
+ printedPageLabel?: string;
319
+ };
320
+ representation: {
321
+ requiredArtifact: CataloguePageRequiredArtifact;
322
+ reason: "retained_representation_not_found";
323
+ status: "unavailable";
324
+ };
325
+ };
326
+ type CataloguePageAvailable = Extract<CataloguePage, {
327
+ representation: {
328
+ status: "available";
329
+ };
330
+ }>;
331
+ type CataloguePageUnavailable = Extract<CataloguePage, {
332
+ representation: {
333
+ status: "unavailable";
334
+ };
335
+ }>;
336
+ export type CatalogPageListResult = {
337
+ catalog: CatalogDocument;
338
+ pages: CataloguePage[];
339
+ pagination: {
340
+ count: number;
341
+ hasMore: boolean;
342
+ limit: number;
343
+ offset: number;
344
+ total: number;
345
+ };
346
+ representationCoverage: {
347
+ available: number;
348
+ failuresByReason: Array<{
349
+ count: number;
350
+ reason: "retained_representation_not_found";
351
+ }>;
352
+ total: number;
353
+ unavailable: number;
354
+ };
355
+ status: "available";
356
+ } | {
357
+ catalog: UnavailableCatalogDocument;
358
+ reason: "retained_source_not_found";
359
+ status: "source_unavailable";
360
+ };
361
+ export type CataloguePageRepresentationReadResult = {
362
+ artifact: CataloguePageAvailableArtifact;
363
+ page: CataloguePageAvailable;
364
+ status: "available";
365
+ } | {
366
+ requiredArtifact: CataloguePageRequiredArtifact;
367
+ page: CataloguePageUnavailable;
368
+ reason: "retained_representation_not_found";
369
+ status: "unavailable";
370
+ } | {
371
+ pageId: string;
372
+ status: "unresolved_page";
373
+ };
374
+ export {};
package/client.d.ts CHANGED
@@ -8,7 +8,7 @@ export type { BrowseNamespace } from "./client-browse.js";
8
8
  export type { ExplorationNamespace } from "./client-exploration.js";
9
9
  import { type SourceMaterialCommentsNamespace } from "./client-source-material-comments.js";
10
10
  export type { SourceMaterialCommentsNamespace } from "./client-source-material-comments.js";
11
- import type { AssociatedOptions } from "./association-types.js";
11
+ import type { ApplicabilityOptions, AssociatedOptions } from "./association-types.js";
12
12
  import type { BrandsOptions } from "./brand-types.js";
13
13
  import type { ByColourParams, ByColourResponse } from "./by-colour-types.js";
14
14
  import { CodesClient } from "./codes-client.js";
@@ -18,8 +18,7 @@ import type { VarySchemesRequest, VarySchemesResponse } from "./composition-type
18
18
  import type { BatchId, ProjectId, RunId, VariantId } from "./identifiers.js";
19
19
  import type { MatchStreamEvent } from "./match-stream-types.js";
20
20
  import type { MatchInput, MatchResponse } from "./match-types.js";
21
- import type { MaterialAssetProjection } from "./material-asset-types.js";
22
- import type { MaterialBlockProjection } from "./material-block-types.js";
21
+ import type { MaterialDocumentPageOptions } from "./material-asset-types.js";
23
22
  import type { MaterialQueryPlan } from "./material-query-plan-types.js";
24
23
  import type { AppendProjectEventRequest, CreateProjectRequest, ProjectEventReceipt, ProjectListResponse, ProjectState } from "./project-types.js";
25
24
  import { resolvePhrasesRequest as resolvePhrases } from "./resolve-client.js";
@@ -217,20 +216,9 @@ export declare class MaterialGraphClient extends CatalogueClient {
217
216
  * goes with this one, and `search({ similarTo })` for one that LOOKS like it.
218
217
  */
219
218
  siblings(variantId: VariantId, signal?: AbortSignal): Promise<SiblingsResponse>;
220
- /**
221
- * THE QUESTION: "what do we actually KNOW about this material, and what have we
222
- * simply not checked?" The only read that distinguishes a determined absence
223
- * from an unasked question — every field carries a `status`, so this is the one
224
- * surface entitled to back a negative ("no fire rating").
225
- * {@link MaterialGraphClient.material} cannot: there an absence is merely unknown.
226
- */
227
- materialBlocks(variantId: VariantId, signal?: AbortSignal): Promise<MaterialBlockProjection>;
228
- /**
229
- * THE QUESTION: "what images and documents may I SHOW for this material?" — the
230
- * redistribution-safe set (GET `/api/v1/materials/{variantId}/assets`), not
231
- * every file on record.
232
- */
233
- materialAssets(variantId: VariantId, signal?: AbortSignal): Promise<MaterialAssetProjection>;
219
+ materialBlocks(variantId: VariantId, signal?: AbortSignal): Promise<import("./material-block-types.js").MaterialBlockProjection>;
220
+ materialAssets(variantId: VariantId, signal?: AbortSignal): Promise<import("./material-asset-types.js").MaterialAssetProjection>;
221
+ materialDocuments(variantId: VariantId, options?: MaterialDocumentPageOptions, signal?: AbortSignal): Promise<import("./material-asset-types.js").MaterialDocumentPage>;
234
222
  /**
235
223
  * THE QUESTION: "what does the schema DEFINE?" — the governed static registry.
236
224
  *
@@ -258,6 +246,7 @@ export declare class MaterialGraphClient extends CatalogueClient {
258
246
  associatedFinishes(variantId: VariantId, options?: AssociatedOptions, signal?: AbortSignal): Promise<import("./association-types.js").DeclaredAssociationsResponse>;
259
247
  /** "What products does the manufacturer declare THIS finish for?" — {@link declaredProducts}. */
260
248
  associatedProducts(variantId: VariantId, options?: AssociatedOptions, signal?: AbortSignal): Promise<import("./association-types.js").DeclaredAssociationsResponse>;
249
+ applicability(options: ApplicabilityOptions, signal?: AbortSignal): Promise<import("./association-types.js").ApplicabilityResponse>;
261
250
  /**
262
251
  * THE QUESTION: "hydrate these ids I already hold" (POST
263
252
  * `/api/v1/materials/batch`, ≤ 100 ids) — never a discovery call. Throws
package/client.js CHANGED
@@ -10,13 +10,14 @@ export * from "./client-config.js";
10
10
  import { browseQuery, createBrowseNamespace, } from "./client-browse.js";
11
11
  import { createExplorationNamespace, } from "./client-exploration.js";
12
12
  import { createSourceMaterialCommentsNamespace, } from "./client-source-material-comments.js";
13
- import { declaredFinishes, declaredProducts } from "./associations-client.js";
13
+ import * as assoc from "./associations-client.js";
14
14
  import { brandColourDeck, brandProfile, listBrands } from "./brands-client.js";
15
15
  import { CodesClient } from "./codes-client.js";
16
16
  import { ColoursClient } from "./colours-client.js";
17
17
  import { slotPools as slotPoolsCall, varyScheme as varySchemeCall, } from "./composition-client.js";
18
18
  import { coordinate as coordinateCall } from "./coordinate-client.js";
19
19
  import { matchStream as matchStreamCall } from "./match-stream.js";
20
+ import { materialAssets as materialAssetsCall, materialBlocks as materialBlocksCall, materialDocuments as materialDocumentsCall, } from "./material-resources-client.js";
20
21
  import { appendProjectEvent as appendProjectEventCall, createProject as createProjectCall, getProject as getProjectCall, listProjects as listProjectsCall, } from "./projects.js";
21
22
  import { resolvePhrasesRequest as resolvePhrases, searchVocabularyRequest, } from "./resolve-client.js";
22
23
  import { SCHEMA_RESOURCE_OPERATION_IDS } from "./schema-resource-types.js";
@@ -275,27 +276,14 @@ export class MaterialGraphClient extends CatalogueClient {
275
276
  siblings(variantId, signal) {
276
277
  return this.#get("get_material_siblings", undefined, signal, { variantId });
277
278
  }
278
- /**
279
- * THE QUESTION: "what do we actually KNOW about this material, and what have we
280
- * simply not checked?" The only read that distinguishes a determined absence
281
- * from an unasked question — every field carries a `status`, so this is the one
282
- * surface entitled to back a negative ("no fire rating").
283
- * {@link MaterialGraphClient.material} cannot: there an absence is merely unknown.
284
- */
285
279
  materialBlocks(variantId, signal) {
286
- return this.#get("list_material_blocks", undefined, signal, {
287
- variantId,
288
- });
280
+ return materialBlocksCall(this.#transport, variantId, signal);
289
281
  }
290
- /**
291
- * THE QUESTION: "what images and documents may I SHOW for this material?" — the
292
- * redistribution-safe set (GET `/api/v1/materials/{variantId}/assets`), not
293
- * every file on record.
294
- */
295
282
  materialAssets(variantId, signal) {
296
- return this.#get("list_material_assets", undefined, signal, {
297
- variantId,
298
- });
283
+ return materialAssetsCall(this.#transport, variantId, signal);
284
+ }
285
+ materialDocuments(variantId, options, signal) {
286
+ return materialDocumentsCall(this.#transport, variantId, options, signal);
299
287
  }
300
288
  /**
301
289
  * THE QUESTION: "what does the schema DEFINE?" — the governed static registry.
@@ -328,11 +316,14 @@ export class MaterialGraphClient extends CatalogueClient {
328
316
  }
329
317
  /** "What finishes does the manufacturer declare for THIS product?" — {@link declaredFinishes}. */
330
318
  associatedFinishes(variantId, options, signal) {
331
- return declaredFinishes(this.#transport, variantId, options, signal);
319
+ return assoc.declaredFinishes(this.#transport, variantId, options, signal);
332
320
  }
333
321
  /** "What products does the manufacturer declare THIS finish for?" — {@link declaredProducts}. */
334
322
  associatedProducts(variantId, options, signal) {
335
- return declaredProducts(this.#transport, variantId, options, signal);
323
+ return assoc.declaredProducts(this.#transport, variantId, options, signal);
324
+ }
325
+ applicability(options, signal) {
326
+ return assoc.applicability(this.#transport, options, signal);
336
327
  }
337
328
  materials(ids, options, signal) {
338
329
  if (ids.length > MAX_BATCH_IDS) {
@@ -29,19 +29,21 @@ import { type OpenEnum } from "./types.js";
29
29
  * `"all"` (and absence) is the WHOLE GRAPH — every material MaterialGraph
30
30
  * knows, orderable or not. Absence is never a narrower default.
31
31
  *
32
- * Three members gate on CHANNEL membership, which is what "can someone buy
33
- * this" means: `"materialBankNaOrderable"` (Material Bank North America),
32
+ * Four members gate on governed CHANNEL membership:
33
+ * `"materialBankNaOrderable"` (Material Bank North America),
34
34
  * `"materialBankEuOrderable"` (Material Bank Europe) and
35
35
  * `"designShopOrderable"` (DesignShop, with the products DesignShop hides from
36
36
  * browse excluded).
37
+ * `"materialBankNaAndCatalogue"` is a single producer-side union of Material
38
+ * Bank North America and governed Catalogue channel membership.
37
39
  *
38
40
  * `"materialBankOrderable"` is NOT one of them, despite the name. It selects
39
41
  * materials carrying a Material Bank identity — PROVENANCE, not a storefront —
40
42
  * and admits about 98% of the graph. Use it to exclude the crawl-sourced brands
41
43
  * that were never in the Material Bank feed; if you mean orderable, use one of
42
- * the three above.
44
+ * the channel-backed scopes above.
43
45
  */
44
- export type CorpusScope = OpenEnum<"all" | "designShopOrderable" | "materialBankEuOrderable" | "materialBankNaOrderable" | "materialBankOrderable">;
46
+ export type CorpusScope = OpenEnum<"all" | "designShopOrderable" | "materialBankEuOrderable" | "materialBankNaAndCatalogue" | "materialBankNaOrderable" | "materialBankOrderable">;
45
47
  /**
46
48
  * The corpus a response AND its counts were drawn from. Present only when a
47
49
  * narrowing scope applied — absence means the whole graph. Read it alongside
@@ -39,10 +39,79 @@ export interface MaterialAssetProjectionDownload {
39
39
  fileExtension: string | null;
40
40
  filename: string | null;
41
41
  }
42
+ export type MaterialDocumentStatus = OpenEnum<"downloaded" | "linked_not_downloaded" | "unavailable_source" | "dead_link" | "failed_ingest">;
43
+ export type MaterialDocumentCoverageState = OpenEnum<"available" | "none_declared" | "not_ingested" | "unavailable_source" | "dead_link" | "failed_ingest">;
44
+ export interface MaterialDocumentScope {
45
+ kind: "product" | "variant";
46
+ id: string;
47
+ }
48
+ export interface MaterialDocumentSource {
49
+ /** Opaque, public identifier for one source collection; never a PIM/DAM path. */
50
+ id: string;
51
+ authority: string;
52
+ revision: string;
53
+ }
54
+ export interface MaterialDocumentProjectionItem {
55
+ id: string;
56
+ scope: MaterialDocumentScope & {
57
+ inherited: boolean;
58
+ };
59
+ declaredScopes: MaterialDocumentScope[];
60
+ variantAttribution: string[];
61
+ /** True only for a Product declaration or this exact requested Variant. */
62
+ appliesToRequestedVariant: boolean;
63
+ /** Every exact source-grain declaration collapsed into this URL identity. */
64
+ declarations: Array<{
65
+ scope: MaterialDocumentScope;
66
+ source: MaterialDocumentSource;
67
+ status: MaterialDocumentStatus;
68
+ readable: boolean;
69
+ }>;
70
+ documentType: string | null;
71
+ originalName: string;
72
+ status: MaterialDocumentStatus;
73
+ /** Null for unavailable, dead, or failed source documents. */
74
+ url: string | null;
75
+ contentType: string | null;
76
+ fileExtension: string | null;
77
+ source: MaterialDocumentSource;
78
+ }
79
+ export interface MaterialDocumentCoverageSource {
80
+ scope: MaterialDocumentScope;
81
+ state: MaterialDocumentCoverageState;
82
+ exact: boolean;
83
+ declaredCount: number | null;
84
+ readableCount: number;
85
+ source: MaterialDocumentSource;
86
+ }
87
+ export interface MaterialDocumentCoverage {
88
+ productUnion: MaterialDocumentCoverageSummary;
89
+ requestedVariant: MaterialDocumentCoverageSummary;
90
+ sources: MaterialDocumentCoverageSource[];
91
+ }
92
+ export interface MaterialDocumentCoverageSummary {
93
+ state: MaterialDocumentCoverageState;
94
+ exact: boolean;
95
+ declaredCount: number | null;
96
+ readableCount: number;
97
+ }
98
+ export interface MaterialDocumentPageOptions {
99
+ cursor?: string;
100
+ limit?: number;
101
+ }
102
+ export interface MaterialDocumentPage {
103
+ variantId: VariantId;
104
+ documents: MaterialDocumentProjectionItem[];
105
+ coverage: MaterialDocumentCoverage;
106
+ totalDocuments: number;
107
+ nextCursor: string | null;
108
+ }
42
109
  /** Tallies over what this projection returned — never a claim about the brand's own library. */
43
110
  export interface MaterialAssetProjectionSummary {
44
111
  imageCount: number;
45
112
  downloadCount: number;
113
+ documentCount: number;
114
+ readableDocumentCount: number;
46
115
  downloadedImageCount: number;
47
116
  }
48
117
  export interface MaterialAssetProjection {
@@ -50,5 +119,7 @@ export interface MaterialAssetProjection {
50
119
  variantId: VariantId;
51
120
  images: MaterialAssetProjectionImage[];
52
121
  downloads: MaterialAssetProjectionDownload[];
122
+ documents: MaterialDocumentProjectionItem[];
123
+ documentCoverage: MaterialDocumentCoverage;
53
124
  summary: MaterialAssetProjectionSummary;
54
125
  }
@@ -0,0 +1,15 @@
1
+ /** Material blocks, safe assets, and technical documents over one shared transport. */
2
+ import { HttpTransport } from "./http-transport.js";
3
+ import type { VariantId } from "./identifiers.js";
4
+ import type { MaterialAssetProjection, MaterialDocumentPage, MaterialDocumentPageOptions } from "./material-asset-types.js";
5
+ import type { MaterialBlockProjection } from "./material-block-types.js";
6
+ /** Read known and explicitly unobserved canonical material fields. */
7
+ export declare function materialBlocks(transport: HttpTransport, variantId: VariantId, signal?: AbortSignal): Promise<MaterialBlockProjection>;
8
+ /** Read only redistribution-safe images and downloads for a material. */
9
+ export declare function materialAssets(transport: HttpTransport, variantId: VariantId, signal?: AbortSignal): Promise<MaterialAssetProjection>;
10
+ /**
11
+ * Read the owning Product's URL-deduplicated document page. Variant declarations
12
+ * retain exact attribution; consumers inspect `appliesToRequestedVariant`
13
+ * before presenting a sibling Variant declaration as applicable.
14
+ */
15
+ export declare function materialDocuments(transport: HttpTransport, variantId: VariantId, options?: MaterialDocumentPageOptions, signal?: AbortSignal): Promise<MaterialDocumentPage>;
@@ -0,0 +1,25 @@
1
+ /** Material blocks, safe assets, and technical documents over one shared transport. */
2
+ import { HttpTransport } from "./http-transport.js";
3
+ import { resolveOperation } from "./operations.js";
4
+ /** Read known and explicitly unobserved canonical material fields. */
5
+ export function materialBlocks(transport, variantId, signal) {
6
+ return transport.requestJson(resolveOperation("list_material_blocks"), { signal, pathParams: { variantId } });
7
+ }
8
+ /** Read only redistribution-safe images and downloads for a material. */
9
+ export function materialAssets(transport, variantId, signal) {
10
+ return transport.requestJson(resolveOperation("list_material_assets"), { signal, pathParams: { variantId } });
11
+ }
12
+ /**
13
+ * Read the owning Product's URL-deduplicated document page. Variant declarations
14
+ * retain exact attribution; consumers inspect `appliesToRequestedVariant`
15
+ * before presenting a sibling Variant declaration as applicable.
16
+ */
17
+ export function materialDocuments(transport, variantId, options, signal) {
18
+ return transport.requestJson(resolveOperation("list_material_documents"), {
19
+ query: options
20
+ ? { cursor: options.cursor, limit: options.limit }
21
+ : undefined,
22
+ signal,
23
+ pathParams: { variantId },
24
+ });
25
+ }
@@ -133,6 +133,41 @@ export interface MaterialDetail extends MaterialTile {
133
133
  installationImageUrl: string | null;
134
134
  displayCategory: string | null;
135
135
  categoryPath: string[];
136
+ /** Durable state of image-colour extraction and its variant projection. */
137
+ colourEvidence: {
138
+ status: "no_eligible_asset";
139
+ } | {
140
+ status: "extractor_not_run";
141
+ origin: {
142
+ assetId: string;
143
+ method: "colorscope_primary_image";
144
+ };
145
+ } | {
146
+ status: "available";
147
+ basis: "palette" | "palette_and_dominant_oklab" | "dominant_oklab";
148
+ extractedAt: string;
149
+ extractionVersion: string | null;
150
+ origin: {
151
+ assetId: string;
152
+ method: "colorscope_primary_image";
153
+ };
154
+ } | {
155
+ status: "projection_missing";
156
+ extractedAt: string;
157
+ extractionVersion: string | null;
158
+ origin: {
159
+ assetId: string;
160
+ method: "colorscope_primary_image";
161
+ };
162
+ } | {
163
+ status: "genuinely_colourless";
164
+ extractedAt: string;
165
+ extractionVersion: string | null;
166
+ origin: {
167
+ assetId: string;
168
+ method: "colorscope_primary_image";
169
+ };
170
+ };
136
171
  /** Extracted from imagery — visual likeness, never a declared colour spec. */
137
172
  palette: PaletteSwatch[];
138
173
  /** What is ON RECORD. An absent attribute is unrecorded, never known-false. */
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 ["catalogs", "GET", "/api/v1/catalogs", "list_catalog_documents"], readonly ["catalogReferences", "GET", "/api/v1/catalogs/{catalogId}/references", "list_catalog_product_references"], 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 ["catalogueReferences", "POST", "/api/v1/catalogues/references", "get_catalogue_references_batch"], readonly ["catalogueDocumentAccess", "GET", "/api/v1/catalogues/documents/{documentId}", "get_catalogue_document_access"], 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 ["catalogs", "GET", "/api/v1/catalogs", "list_catalog_documents"], readonly ["catalogPages", "GET", "/api/v1/catalogs/{catalogId}/pages", "list_catalog_pages"], readonly ["catalogPageRepresentation", "GET", "/api/v1/catalog-pages/{pageId}/representation", "get_catalog_page_representation"], readonly ["catalogReferences", "GET", "/api/v1/catalogs/{catalogId}/references", "list_catalog_product_references"], 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 ["catalogueReferences", "POST", "/api/v1/catalogues/references", "get_catalogue_references_batch"], readonly ["catalogueCollections", "GET", "/api/v1/catalogues/collections", "list_catalogue_channel_collections"], readonly ["catalogueDocumentAccess", "GET", "/api/v1/catalogues/documents/{documentId}", "get_catalogue_document_access"], 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 ["materialDocuments", "GET", "/api/v1/materials/{variantId}/documents", "list_material_documents"], 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 ["applicability", "GET", "/api/v1/applicability", "get_applicability"], 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,18 @@ import { MaterialGraphError } from "./errors.js";
2
2
  export const MATERIALGRAPH_API_VERSION = "0.7.0";
3
3
  const OPERATION_DEFINITIONS = [
4
4
  ["catalogs", "GET", "/api/v1/catalogs", "list_catalog_documents"],
5
+ [
6
+ "catalogPages",
7
+ "GET",
8
+ "/api/v1/catalogs/{catalogId}/pages",
9
+ "list_catalog_pages",
10
+ ],
11
+ [
12
+ "catalogPageRepresentation",
13
+ "GET",
14
+ "/api/v1/catalog-pages/{pageId}/representation",
15
+ "get_catalog_page_representation",
16
+ ],
5
17
  [
6
18
  "catalogReferences",
7
19
  "GET",
@@ -20,6 +32,12 @@ const OPERATION_DEFINITIONS = [
20
32
  "/api/v1/catalogues/references",
21
33
  "get_catalogue_references_batch",
22
34
  ],
35
+ [
36
+ "catalogueCollections",
37
+ "GET",
38
+ "/api/v1/catalogues/collections",
39
+ "list_catalogue_channel_collections",
40
+ ],
23
41
  [
24
42
  "catalogueDocumentAccess",
25
43
  "GET",
@@ -44,6 +62,12 @@ const OPERATION_DEFINITIONS = [
44
62
  "/api/v1/materials/{variantId}/assets",
45
63
  "list_material_assets",
46
64
  ],
65
+ [
66
+ "materialDocuments",
67
+ "GET",
68
+ "/api/v1/materials/{variantId}/documents",
69
+ "list_material_documents",
70
+ ],
47
71
  [
48
72
  "exploration.operators",
49
73
  "GET",
@@ -103,6 +127,7 @@ const OPERATION_DEFINITIONS = [
103
127
  "/api/v1/associated/products",
104
128
  "list_associated_products",
105
129
  ],
130
+ ["applicability", "GET", "/api/v1/applicability", "get_applicability"],
106
131
  ["materials", "POST", "/api/v1/materials/batch", "get_materials_batch_post"],
107
132
  [
108
133
  "materialsCached",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -63,7 +63,7 @@ export interface CanonicalLexicalReading {
63
63
  export interface CanonicalLexicalMatch {
64
64
  original: string;
65
65
  canonical: string;
66
- rule: OpenEnum<"exact" | "plural-to-singular">;
66
+ rule: OpenEnum<"exact" | "plural-to-singular" | "typo-to-canonical">;
67
67
  span: {
68
68
  start: number;
69
69
  end: number;
@@ -73,7 +73,7 @@ export interface CanonicalLexicalMatch {
73
73
  receipt: {
74
74
  original: string;
75
75
  canonical: string;
76
- rule: OpenEnum<"exact" | "plural-to-singular">;
76
+ rule: OpenEnum<"exact" | "plural-to-singular" | "typo-to-canonical">;
77
77
  authorities: CanonicalLexicalAuthorityReceipt[];
78
78
  };
79
79
  }
@@ -158,6 +158,7 @@ export interface QueryProductTypeReceipt {
158
158
  export interface QueryArmReceipt {
159
159
  arm: OpenEnum<"dense" | "lexical" | "colour"> | null;
160
160
  reason: string;
161
+ reasonDetail?: OpenEnum<"derived-colour-veto" | "typo-recovery" | "literal-compound-without-type">;
161
162
  outcome: OpenEnum<"dense" | "lexical" | "colour" | "browse" | "criteria-only" | "ambiguous">;
162
163
  decidedBy: OpenEnum<"deterministic" | "model">;
163
164
  productTypeGate: OpenEnum<"hard" | "constrain" | "none">;
@@ -189,8 +190,10 @@ export interface QueryCategoryScopeReceipt {
189
190
  export interface QuerySpecAllowlistReceipt {
190
191
  limit: number;
191
192
  returned: number;
193
+ retrieved?: number;
192
194
  truncated: boolean;
193
195
  emptyMeansScarcity: boolean;
196
+ outcome?: OpenEnum<"corpus-scarcity" | "retrieval-miss" | "unknown">;
194
197
  }
195
198
  export interface QueryUnsatisfiableNarrowing {
196
199
  narrowing: OpenEnum<"product-type">;
@@ -50,7 +50,7 @@ export interface CollapsedVariant {
50
50
  * ABSENT means no MB identity is on record, never that it cannot be ordered.
51
51
  */
52
52
  materialBankId?: string;
53
- /** This variant's own fused score — the value the array is ordered by. */
53
+ /** This variant's own mode-primary score — the value the array is ordered by. */
54
54
  score: number;
55
55
  }
56
56
  /**
@@ -32,8 +32,8 @@ export interface SearchInput {
32
32
  };
33
33
  mood?: string;
34
34
  /**
35
- * Re-order the final scored results. `relevance` (default) keeps the fused
36
- * score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
35
+ * Re-order the final scored results. `relevance` (default) keeps the selected
36
+ * mode's primary score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
37
37
  * ramps; `name` (A→Z), `name_desc` (Z→A) and `recent` (newest first) are the
38
38
  * catalogue orders — the same three `/search/browse` walks the corpus in, so
39
39
  * one word means one order on both lanes.
@@ -100,12 +100,18 @@ export interface SearchInput {
100
100
  boost?: string;
101
101
  filter?: string[];
102
102
  };
103
+ /**
104
+ * Optional multipliers for the selected mode's one ordering signal. At most
105
+ * one is applied: image for image/image-text, lexical for lexical/browse, and
106
+ * colour for colour mode. Non-mode keys are accepted for compatibility but
107
+ * cannot activate secondary scoring.
108
+ */
103
109
  weights?: {
104
110
  image?: number;
105
111
  category?: number;
106
112
  colour?: number;
107
113
  lexical?: number;
108
- /** Text rerank multiplier applied when a seeded search also carries `text` (default 0.4). */
114
+ /** Compatibility field; seeded text does not activate secondary scoring. */
109
115
  text?: number;
110
116
  };
111
117
  filters?: SearchFilter[];
@@ -120,6 +126,7 @@ export interface SearchInput {
120
126
  /** The previous response's `cursor`, passed back verbatim. Never synthesise one. */
121
127
  cursor?: Cursor;
122
128
  explain?: boolean;
129
+ /** Public POST defaults to distinct products; send `"none"` for individual variants. */
123
130
  collapse?: OpenEnum<"none" | "product">;
124
131
  /**
125
132
  * Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools.
@@ -156,7 +163,7 @@ export interface SearchInput {
156
163
  * `"search"` (default) is the full engine. `"typeahead"` is the as-you-type
157
164
  * profile: lexical-only with prefix match on the final token, forced
158
165
  * `view: "card"`, `limit` capped at 24, and no mood/colour detection tiers,
159
- * semantic fallback, category boost, colour rerank, refinement suggestions,
166
+ * semantic fallback, secondary scoring, refinement suggestions,
160
167
  * counts, or meanings. `textModes` is ignored under typeahead.
161
168
  */
162
169
  intent?: OpenEnum<"search" | "typeahead">;
@@ -166,22 +173,26 @@ export interface SearchInput {
166
173
  * **ABSENT means the WHOLE GRAPH** — every material MaterialGraph knows,
167
174
  * orderable or not. Absence is never a narrower default, so a caller that
168
175
  * says nothing gets materials it may not be able to buy.
169
- * Three scopes gate on CHANNEL membership, which is what "can someone buy
170
- * this" means: `"materialBankNaOrderable"` (Material Bank North America,
176
+ * Three transaction-oriented scopes gate on governed CHANNEL membership:
177
+ * `"materialBankNaOrderable"` (Material Bank North America,
171
178
  * ~277,000 materials), `"materialBankEuOrderable"` (Material Bank Europe,
172
179
  * ~65,000) and `"designShopOrderable"` (DesignShop, ~34,000, with the
173
180
  * products DesignShop hides from browse excluded).
181
+ * `"materialBankNaAndCatalogue"` is a governed discovery corpus: the single
182
+ * producer-side union of Material Bank North America and Catalogue editorial
183
+ * membership. It is not a buyability claim, and a catalogue reference alone
184
+ * does not establish membership.
174
185
  *
175
186
  * `"materialBankOrderable"` is NOT one of them, despite the name. It selects
176
187
  * materials carrying a Material Bank identity — a PROVENANCE signal, not a
177
188
  * storefront — and admits about 98% of the graph. Use it to exclude the
178
189
  * crawl-sourced brands that were never in the Material Bank feed; if you mean
179
- * orderable, use one of the three above.
190
+ * orderable, use one of the channel-backed scopes above.
180
191
  *
181
- * A scope answers what is BUYABLE. It never answers what is SUITABLE: scope
182
- * on channel, then refine on sector. A trade-only material can be right for a
183
- * room and still unbuyable, and most of what a consumer can buy is
184
- * commercial-suited product.
192
+ * A scope answers membership in its named distribution or visibility
193
+ * surface; the token-specific description states whether that means
194
+ * transaction or editorial inclusion. It never answers what is SUITABLE:
195
+ * scope on channel, then refine on sector.
185
196
  *
186
197
  * The scope is applied INSIDE the query: `limit` fills with eligible rows,
187
198
  * `counts` describe the SCOPED corpus, and the response echoes the scope.
@@ -203,6 +214,8 @@ export interface SearchGetParams {
203
214
  colours?: string;
204
215
  logic?: OpenEnum<"all" | "any">;
205
216
  mood?: string;
217
+ /** Governed corpus applied inside retrieval; absent means the whole graph. */
218
+ corpusScope?: CorpusScope;
206
219
  sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
207
220
  tol?: OpenEnum<"tight" | "normal" | "loose">;
208
221
  /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
@@ -1,17 +1,55 @@
1
1
  /**
2
2
  * @module sdk/search-response-receipt-types
3
- * @description Inspectable routing, rerank, and compound-constraint receipts
4
- * emitted by ordinary Search responses.
3
+ * @description Inspectable routing, compatibility, and compound-constraint
4
+ * receipts emitted by ordinary Search responses.
5
5
  */
6
6
  import type { OpenEnum } from "./types.js";
7
7
  /** The primary ranking source a query resolved to and reports as its mode. */
8
8
  export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse">;
9
- /** Rerank-term report (absent in colour mode — there colour IS the source). */
9
+ /**
10
+ * Compatibility receipt for non-mode colour handling. Current one-signal
11
+ * ordering reports it skipped at weight 0; colour may still be diagnostic or a
12
+ * hard displayed-image constraint, but does not blend into score.
13
+ */
10
14
  export interface SearchRerankReport {
11
15
  colour?: OpenEnum<"ok" | "failed" | "skipped">;
12
16
  colourWeight?: number;
13
17
  colourLabel?: string;
14
18
  }
19
+ /** Stage counts retained when an explain-enabled search returns no results. */
20
+ export interface SearchEmptyOutcomeCounts {
21
+ retrieved: number;
22
+ afterPrune: number;
23
+ final: number;
24
+ }
25
+ /**
26
+ * Explain-only evidence for why a completed search returned no results.
27
+ * `proven-corpus-empty` is exhaustive; every other empty remains explicitly
28
+ * unproven rather than being presented as catalogue absence.
29
+ */
30
+ export type SearchEmptyOutcome = {
31
+ kind: "proven-corpus-empty";
32
+ basis: OpenEnum<"spec-allowlist" | "brand-scope">;
33
+ exhaustive: true;
34
+ sourceEmpty?: boolean | null;
35
+ counts?: SearchEmptyOutcomeCounts;
36
+ } | {
37
+ kind: "unproven-empty";
38
+ reason: OpenEnum<"allowlist-truncated" | "retrieval-window-not-exhaustive" | "source-empty" | "post-retrieval-rejected" | "pagination-empty" | "no-authoritative-empty-proof">;
39
+ sourceEmpty?: boolean | null;
40
+ counts?: SearchEmptyOutcomeCounts;
41
+ };
42
+ /** Explain-only coverage of substantive query terms in the lexical projection. */
43
+ export interface SearchTermCoverageReceipt {
44
+ status: OpenEnum<"covered" | "uncovered" | "unavailable" | "skipped">;
45
+ terms: string[];
46
+ note?: string;
47
+ }
48
+ /** Explain-only outcome receipts shared by full and card search responses. */
49
+ export interface SearchResponseExplainReceipts {
50
+ emptyOutcome?: SearchEmptyOutcome;
51
+ termCoverage?: SearchTermCoverageReceipt;
52
+ }
15
53
  /** One stage in the explain-only ledger of transformations that actually ran. */
16
54
  export type SearchQueryTransformStage = {
17
55
  stage: OpenEnum<"normalise" | "resolve-spec" | "resolve-paint" | "resolve-brand">;
@@ -63,7 +101,10 @@ export interface SearchBrandSkip {
63
101
  */
64
102
  export interface SearchExecutionPlan {
65
103
  grain: OpenEnum<"variant" | "product">;
104
+ /** Which 2048D Voyage projection served a text-mode request. */
105
+ textLane?: OpenEnum<"image-text" | "variant-text">;
66
106
  weights: {
107
+ portrait: number;
67
108
  image: number;
68
109
  category: number;
69
110
  colour: number;
@@ -71,6 +112,13 @@ export interface SearchExecutionPlan {
71
112
  text: number;
72
113
  };
73
114
  queryTransform: SearchQueryTransformStage[];
115
+ /** Ordered, explain-only request-understanding execution trace. */
116
+ steps?: Array<{
117
+ key: string;
118
+ status: OpenEnum<"executed" | "skipped">;
119
+ reason: string;
120
+ durationMs: number;
121
+ }>;
74
122
  categoryAnchor?: SearchCategoryAnchor;
75
123
  brandAnchor?: SearchBrandAnchor;
76
124
  brandSkipped?: SearchBrandSkip;
@@ -73,9 +73,12 @@ function checkColourMeta(value, what, fail) {
73
73
  requireOptionalString(record.dominantFamily, `${what}.dominantFamily`, fail);
74
74
  requireOptionalString(record.dominantHex, `${what}.dominantHex`, fail);
75
75
  }
76
- /** `searchResultSignalsSchema` — five nullable numbers, all required keys. */
76
+ /** `searchResultSignalsSchema` — five required scores plus additive portrait. */
77
77
  function checkSignals(value, what, fail) {
78
78
  const record = requireRecord(value, what, fail);
79
+ if (record.portrait !== undefined) {
80
+ requireNullableNumber(record.portrait, `${what}.portrait`, fail);
81
+ }
79
82
  for (const signal of ["image", "category", "colour", "lexical", "text"]) {
80
83
  requireNullableNumber(record[signal], `${what}.${signal}`, fail);
81
84
  }
@@ -4,14 +4,22 @@ export interface SearchWindowReceipt {
4
4
  profile: "interactive-r1" | (string & {});
5
5
  /** Candidate depth used by the final retrieval. */
6
6
  candidateTier: number;
7
- /** Every candidate retrieval, including required fallbacks and optional recovery. */
7
+ /** Candidate retrieval count; `searchMaterials` reports exactly one selected retrieval. */
8
8
  retrievalRounds: number;
9
9
  /** Candidate-retrieval wall time only, in milliseconds. */
10
10
  retrievalMs: number;
11
11
  /** True when the serving source proved there were no more candidates. */
12
12
  exhaustive: boolean;
13
- /** True when product-grain scarcity caused one jump to the deep ceiling. */
14
- deepened: boolean;
15
- /** True when a still-short page exhausted its bounded recovery allowance. */
16
- budgetExhausted: boolean;
13
+ /**
14
+ * Candidate attrition through the selected retrieval source. Absent when the
15
+ * response path did not perform ordinary retrieval (for example typeahead).
16
+ */
17
+ candidateCounts?: {
18
+ /** Provider-returned matches before bounded selection; null for SQL/exact arms. */
19
+ rawProviderMatches: number | null;
20
+ /** Matches selected from that provider response; null for SQL/exact arms. */
21
+ selectedMatches: number | null;
22
+ /** Canonical candidates returned by the selected retrieval arm. */
23
+ hydratedCanonicalCandidates: number;
24
+ };
17
25
  }
@@ -12,6 +12,7 @@
12
12
  * `mg.searchVocabulary()` covers attribute/value tokens. Suggestions are a page
13
13
  * of the best matches, never a census of matching entities.
14
14
  */
15
+ import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
15
16
  /** Which kind of entity a suggestion is — and therefore what `refId` means. */
16
17
  export type SuggestKind = "product" | "brand" | "category";
17
18
  /** How the query met the suggestion's label. Strongest first. */
@@ -44,6 +45,8 @@ export interface SuggestResponse {
44
45
  query: string;
45
46
  /** Ranked best-match-first. Empty when nothing came close — a miss, not an error. */
46
47
  suggestions: Suggestion[];
48
+ /** Present only when a narrowing governed corpus was applied. */
49
+ corpusScope?: CorpusScopeReceipt;
47
50
  }
48
51
  /** Options for {@link MaterialGraphClient.suggest}. */
49
52
  export interface SuggestOptions {
@@ -51,6 +54,8 @@ export interface SuggestOptions {
51
54
  limit?: number;
52
55
  /** Restrict to these kinds. Omit for all of them. */
53
56
  kind?: SuggestKind | SuggestKind[];
57
+ /** Governed corpus membership; absent means the whole graph. */
58
+ corpusScope?: CorpusScope;
54
59
  }
55
60
  /**
56
61
  * Shape the query string for a suggest call.
@@ -64,4 +69,5 @@ export declare function suggestQuery(q: string, options?: SuggestOptions): {
64
69
  q: string;
65
70
  limit?: number;
66
71
  kind?: string;
72
+ corpusScope?: CorpusScope;
67
73
  };
package/suggest-types.js CHANGED
@@ -24,5 +24,5 @@ export function suggestQuery(q, options) {
24
24
  const kind = Array.isArray(options?.kind)
25
25
  ? options.kind.join(",")
26
26
  : options?.kind;
27
- return { q, limit: options?.limit, kind };
27
+ return { q, limit: options?.limit, kind, corpusScope: options?.corpusScope };
28
28
  }
package/types.d.ts CHANGED
@@ -32,7 +32,7 @@ import type { MaterialDetail, MaterialTile } from "./material-types.js";
32
32
  import type { QueryUnderstanding } from "./query-understanding-types.js";
33
33
  import type { CollapsedVariant, SearchAnatomyMember } from "./search-collapse-types.js";
34
34
  import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
35
- import type { CompoundColourConstraintReceipt, CompoundFormConstraintReceipt, SearchExecutionPlan, SearchMode, SearchRerankReport } from "./search-response-receipt-types.js";
35
+ import type { CompoundColourConstraintReceipt, CompoundFormConstraintReceipt, SearchExecutionPlan, SearchMode, SearchRerankReport, SearchResponseExplainReceipts } from "./search-response-receipt-types.js";
36
36
  import type { SearchWindowReceipt } from "./search-window-types.js";
37
37
  export type * from "./search-response-receipt-types.js";
38
38
  export type OpenEnum<T extends string> = T | (string & {});
@@ -90,11 +90,13 @@ export type ImageSource = OpenEnum<"r2" | "origin">;
90
90
  export type { DimensionEntry, MaterialAvailability, MaterialDetail, MaterialFacet, MaterialSuccessor, MaterialTile, } from "./material-types.js";
91
91
  /** Per-signal scores, present on a result when `explain` was requested. */
92
92
  export interface SearchResultSignals {
93
+ /** Voyage similarity in the dedicated variant text-portrait space. */
94
+ portrait?: number | null;
93
95
  image: number | null;
94
96
  category: number | null;
95
97
  colour: number | null;
96
98
  lexical: number | null;
97
- /** Text→image RERANK cosine — non-null only for a seeded search carrying `text`. */
99
+ /** Compatibility slot for retired seeded secondary scoring; currently null. */
98
100
  text: number | null;
99
101
  }
100
102
  /** An accepted/measured visual meaning — descriptive evidence, never a ranking cause. */
@@ -190,7 +192,7 @@ export interface SearchResultItem {
190
192
  siblings?: SearchAnatomyMember[];
191
193
  siblingsTruncated?: boolean;
192
194
  };
193
- /** Fused score — range [0, sum of active weights]; see `maxScore`. */
195
+ /** Selected mode's weighted primary score; see `maxScore`. */
194
196
  score: number;
195
197
  /**
196
198
  * Match only. How close this material is to the query, as an ABSOLUTE
@@ -398,7 +400,7 @@ export interface SearchRefinementReceipt {
398
400
  * you asked for it. Follow `cursor` for the rest. "I found 24 materials" is
399
401
  * wrong whenever `limit` was 24.
400
402
  */
401
- export interface SearchResponse {
403
+ export interface SearchResponse extends SearchResponseExplainReceipts {
402
404
  view: OpenEnum<"full" | "card">;
403
405
  /** THIS PAGE. Its length is a page size, never a total. */
404
406
  results: SearchResultItem[];