@instruments/materialgraph 0.9.1 → 0.11.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.
Files changed (64) hide show
  1. package/brands-client.d.ts +48 -0
  2. package/brands-client.js +45 -0
  3. package/browse-types.d.ts +147 -0
  4. package/browse-types.js +1 -0
  5. package/by-colour-types.d.ts +105 -5
  6. package/client-browse.d.ts +13 -3
  7. package/client-browse.js +12 -2
  8. package/client-exploration.d.ts +9 -0
  9. package/client-exploration.js +7 -0
  10. package/client-source-material-comments.d.ts +51 -0
  11. package/client-source-material-comments.js +40 -0
  12. package/client.d.ts +64 -71
  13. package/client.js +90 -78
  14. package/colour-aggregate-types.d.ts +58 -0
  15. package/colours-client.d.ts +8 -2
  16. package/colours-client.js +15 -2
  17. package/composition-client.d.ts +33 -0
  18. package/composition-client.js +27 -0
  19. package/composition-types.d.ts +46 -0
  20. package/coordinate-client.d.ts +23 -0
  21. package/coordinate-client.js +25 -0
  22. package/errors.js +24 -8
  23. package/exploration-types.d.ts +110 -0
  24. package/exploration-types.js +1 -0
  25. package/fan-deck-types.d.ts +42 -0
  26. package/fan-deck-types.js +9 -0
  27. package/http-transport.js +24 -5
  28. package/index.d.ts +7 -3
  29. package/index.js +2 -2
  30. package/match-stream-types.d.ts +1 -1
  31. package/match-stream-types.js +16 -3
  32. package/match-stream.js +14 -4
  33. package/material-types.d.ts +1 -1
  34. package/operations.d.ts +1 -1
  35. package/operations.js +187 -26
  36. package/package.json +1 -1
  37. package/palette-search-types.d.ts +31 -0
  38. package/resolve-client.d.ts +22 -3
  39. package/resolve-client.js +31 -4
  40. package/resolve-types.d.ts +70 -1
  41. package/run-events.d.ts +1 -1
  42. package/run-events.js +4 -1
  43. package/run-types.d.ts +132 -0
  44. package/runs.d.ts +35 -1
  45. package/runs.js +69 -5
  46. package/schedule-types.d.ts +128 -0
  47. package/schema-resource-types.d.ts +1 -1
  48. package/search-refinement-types.d.ts +39 -4
  49. package/search-request-types.d.ts +172 -0
  50. package/search-request-types.js +1 -0
  51. package/search-result-validation.js +21 -4
  52. package/slot-pool-types.d.ts +370 -0
  53. package/slot-pool-types.js +1 -0
  54. package/source-material-validation.d.ts +5 -1
  55. package/source-material-validation.js +114 -5
  56. package/spec-facet-types.d.ts +53 -0
  57. package/spec-facet-types.js +7 -0
  58. package/specifications.d.ts +26 -0
  59. package/specifications.js +32 -0
  60. package/sse.js +3 -1
  61. package/suggest-types.js +3 -1
  62. package/types.d.ts +176 -216
  63. package/validation.d.ts +12 -0
  64. package/validation.js +23 -0
package/client.d.ts CHANGED
@@ -1,25 +1,31 @@
1
- import { type RateLimit } from "./errors.js";
2
1
  import type { MaterialGraphClientOptions } from "./client-config.js";
2
+ import { type RateLimit } from "./errors.js";
3
3
  export * from "./client-config.js";
4
4
  import { type BrowseNamespace } from "./client-browse.js";
5
+ import { type ExplorationNamespace } from "./client-exploration.js";
5
6
  export type { BrowseNamespace } from "./client-browse.js";
6
- import type { CapabilityManifestResponse, CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, ListRunsOptions, PublicRunListResponse, PublicRunResponse, ResumeRunOptions, RunEvent, SourceMaterialBatchDetail, SourceMaterialBatchReceipt, StreamRunEventsOptions, UploadSourceMaterialInput } from "./run-types.js";
7
- import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
8
- import type { MatchStreamEvent } from "./match-stream-types.js";
9
- import type { AppendProjectEventRequest, CreateProjectRequest, ProjectEventReceipt, ProjectListResponse, ProjectState } from "./project-types.js";
10
- import type { VarySchemesRequest, VarySchemesResponse } from "./composition-types.js";
11
- import { CodesClient } from "./codes-client.js";
12
- import { ColoursClient } from "./colours-client.js";
13
- import type { CodeAdvisoryInput, CodeAdvisoryResponse } from "./codes-types.js";
7
+ export type { ExplorationNamespace } from "./client-exploration.js";
8
+ import { type SourceMaterialCommentsNamespace } from "./client-source-material-comments.js";
9
+ export type { SourceMaterialCommentsNamespace } from "./client-source-material-comments.js";
14
10
  import type { AssociatedOptions } from "./association-types.js";
15
11
  import type { BrandCatalogListPage, BrandsOptions } from "./brand-types.js";
12
+ import type { ByColourParams, ByColourResponse } from "./by-colour-types.js";
13
+ import { CodesClient } from "./codes-client.js";
14
+ import type { CodeAdvisoryInput, CodeAdvisoryResponse } from "./codes-types.js";
15
+ import { ColoursClient } from "./colours-client.js";
16
+ import type { VarySchemesRequest, VarySchemesResponse } from "./composition-types.js";
17
+ import type { BatchId, ProjectId, RunId, VariantId } from "./identifiers.js";
18
+ import type { MatchStreamEvent } from "./match-stream-types.js";
19
+ import type { MatchInput, MatchResponse } from "./match-types.js";
16
20
  import type { MaterialAssetProjection } from "./material-asset-types.js";
17
21
  import type { MaterialBlockProjection } from "./material-block-types.js";
22
+ import type { AppendProjectEventRequest, CreateProjectRequest, ProjectEventReceipt, ProjectListResponse, ProjectState } from "./project-types.js";
23
+ import { resolvePhrasesRequest as resolvePhrases } from "./resolve-client.js";
24
+ import type { CapabilityManifestResponse, CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, ListRunsOptions, PublicRunListResponse, PublicRunResponse, ResumeRunOptions, RunEvent, SourceMaterialBatchDetail, SourceMaterialBatchReceipt, StreamRunEventsOptions, UploadSourceMaterialInput } from "./run-types.js";
25
+ import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
18
26
  import type { SchemaResource, SchemaResourceResponse } from "./schema-resource-types.js";
19
- import type { BatchId, ProjectId, RunId, VariantId } from "./identifiers.js";
20
- import type { ByColourParams, ByColourResponse } from "./by-colour-types.js";
27
+ import type { SlotPoolsRequest, SlotPoolsResponse } from "./slot-pool-types.js";
21
28
  import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandBatchInput, DemandBatchResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, SuggestOptions, SuggestResponse, TaxonomyResponse, VocabularySearchOptions, WhoamiResponse } from "./types.js";
22
- import type { MatchInput, MatchResponse } from "./match-types.js";
23
29
  /**
24
30
  * The MaterialGraph client — every lens on the graph, on one object.
25
31
  *
@@ -60,14 +66,21 @@ export declare class MaterialGraphClient {
60
66
  #private;
61
67
  readonly baseUrl: string;
62
68
  /**
63
- * THE QUESTION: "show me everything, ordered lightest / most vivid / most
64
- * muted." Pure colour scalars — no query, no relevance. `browse({ sort })` is
65
- * one page; `browse.pages({ sort })` walks them all, and only the walk can
66
- * support a corpus claim.
69
+ * THE QUESTION: "show me everything in this category / this brand, in a plain
70
+ * order." No query, no relevance, no ranking. `browse(params)` is one page;
71
+ * `browse.pages(params)` walks them all.
72
+ *
73
+ * `sort` is optional (`name` A→Z by default, `recent` for newest first, plus
74
+ * the five colour ramps), `types`/`brands`/`corpusScope` narrow it, and
75
+ * `count: true` returns the total so a listing can say how many things it is
76
+ * listing without walking every page. `results.length` remains a PAGE.
67
77
  *
68
78
  * Instead use {@link MaterialGraphClient.byColour} for materials NEAR a colour,
69
79
  * {@link MaterialGraphClient.search} when there is anything to rank against,
70
80
  * and `colours.profiles()` for a category's palette rather than its members.
81
+ *
82
+ * @example A category listing that can state its own size
83
+ * const page = await client.browse({ types: "tile_surfaces", count: true });
71
84
  */
72
85
  readonly browse: BrowseNamespace;
73
86
  /**
@@ -93,6 +106,14 @@ export declare class MaterialGraphClient {
93
106
  * likeness, never a declared colour specification.
94
107
  */
95
108
  readonly colours: ColoursClient;
109
+ /** Discover honest graph questions, then traverse one explicit operator. */
110
+ readonly exploration: ExplorationNamespace;
111
+ /**
112
+ * THE QUESTION: "what did people say about this uploaded file, and record what
113
+ * they said." Names on these remarks are REPORTED, never verified — see
114
+ * {@link SourceMaterialCommentsNamespace}.
115
+ */
116
+ readonly sourceMaterialComments: SourceMaterialCommentsNamespace;
96
117
  constructor(options: MaterialGraphClientOptions);
97
118
  get lastRateLimit(): RateLimit | null;
98
119
  /**
@@ -172,8 +193,7 @@ export declare class MaterialGraphClient {
172
193
  * {@link MaterialGraphClient.browse} to walk by lightness/vividness, and
173
194
  * {@link MaterialGraphClient.search} when colour is one signal among many.
174
195
  * Report matches as visual likeness, never a stated colour specification.
175
- * `family` gates the answer to ONE colorscope colour family when nearness
176
- * alone crosses a hue boundary; a gated page can be shorter than `limit`.
196
+ * `family` gates to ONE colorscope family (a gated page can be short); `brand` gates to named brands — one brand's nearest colour to another's.
177
197
  */
178
198
  byColour(params: ByColourParams, signal?: AbortSignal): Promise<ByColourResponse>;
179
199
  /**
@@ -326,33 +346,29 @@ export declare class MaterialGraphClient {
326
346
  resolveAttributeValue(attributeId: string, query: string, options?: ResolveAttributeValueOptions, signal?: AbortSignal): Promise<ResolveAttributeValueResponse>;
327
347
  /** "Is this phrase an attribute or a value?" — see {@link searchVocabularyRequest}. */
328
348
  searchVocabulary(query: string, options?: VocabularySearchOptions, signal?: AbortSignal): Promise<import("./catalog-discovery-types.js").VocabularySearchResponse>;
329
- /** "I have words — what do they mean in YOUR vocabulary?" — see {@link resolvePhrasesRequest}. */
330
- resolve(phrases: string[], signal?: AbortSignal): Promise<import("./resolve-types.js").ResolveResponse>;
349
+ /** "I have words — what do they mean, and what stands behind them?" — see {@link resolvePhrases}. */
350
+ resolve(phrases: string[], scope?: Parameters<typeof resolvePhrases>[2]): Promise<import("./resolve-types.js").ResolveResponse>;
331
351
  /**
332
352
  * THE QUESTION: "which materials, from ANYONE, are ASTM E84 Class A AND
333
- * GREENGUARD-certified?" — hard specifications, no ranking, every brand at once
334
- * whatever each called the value in source.
353
+ * GREENGUARD-certified?" — hard specifications, no ranking, every brand at
354
+ * once whatever each called the value in source. The value-anchored,
355
+ * spec-only primitive (GET `/api/v1/search/specifications`).
335
356
  *
336
357
  * Instead use `search({ filters })` when the spec merely GATES a ranked
337
358
  * question, `codes.advisory()` when the real question is what a space demands,
338
- * and {@link MaterialGraphClient.specificationFacets} first if you do not know
339
- * the canonical tokens. Guessing one no longer returns a silently empty
340
- * answer: an unresolvable criterion means the query NEVER RAN, and the
341
- * response says so — read {@link BySpecResponse.unresolvedCriteria} before
342
- * treating an empty `matches` as "nothing qualifies".
343
- *
344
- * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
345
- * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
346
- *
347
- * Find materials by canonical specification across every brand — the
348
- * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
349
- * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
350
- * them, and each match carries per-criterion trust (`confidence` +
351
- * `sourceBasis`). Answers "which materials, from anyone, are ASTM E84 Class A
352
- * AND GREENGUARD-certified?" Build the strings with `specCriterion` /
353
- * `certificationProgrammeCriterion` (e.g.
354
- * `certificationProgrammeCriterion("GREENGUARD Gold")`); discover the queryable
355
- * vocabulary with {@link specificationFacets} and {@link attributeValues}.
359
+ * and {@link specificationFacets} first if you do not know the canonical
360
+ * tokens. Guessing one no longer returns a silently empty answer: an
361
+ * unresolvable criterion means the query NEVER RAN and the response says so —
362
+ * read {@link BySpecResponse.unresolvedCriteria} before treating an empty
363
+ * `matches` as "nothing qualifies". A PAGE, NOT A CORPUS, unusually so:
364
+ * `totalMatches` is the returned sample's size under `limit`, not a corpus
365
+ * count. Each `spec` is an `attribute:value` criterion built with
366
+ * `specCriterion` / `certificationProgrammeCriterion`; a variant must satisfy
367
+ * ALL of them, and each match carries per-criterion trust (`confidence` +
368
+ * `sourceBasis`). NUMBER-valued specs take BOUNDS, not values — pass
369
+ * `options.ranges` built with `specRangeCriterion("light_reflectance_value",
370
+ * { min: 60 })`; bounds are inclusive, at least one is required, and a
371
+ * non-numeric stored value is excluded rather than read as zero.
356
372
  */
357
373
  bySpec(spec: string | string[], options?: BySpecOptions, signal?: AbortSignal): Promise<BySpecResponse>;
358
374
  /**
@@ -368,17 +384,7 @@ export declare class MaterialGraphClient {
368
384
  * and `mg:admin` unlocks extra evidence fields.
369
385
  */
370
386
  whoami(signal?: AbortSignal): Promise<WhoamiResponse>;
371
- /**
372
- * THE QUESTION: "here is my whole scheme so far — what completes it?" The
373
- * multi-seed form of {@link MaterialGraphClient.pairings}: lift is summed across
374
- * seeds, so a companion suiting one pick ranks below one suiting several.
375
- * Instead use `pairings` for a single material and `colours.palette()` when the
376
- * scheme is COLOURS rather than materials. Check `unresolved` before presenting.
377
- *
378
- * Coordinate a palette of already-chosen materials (POST
379
- * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
380
- * colour swatches and an annotate-only `colourFit` (never reorders in v1).
381
- */
387
+ /** What completes a scheme already part-chosen. See {@link coordinateCall}. */
382
388
  coordinate(input: CoordinateInput, signal?: AbortSignal): Promise<CoordinateResponse>;
383
389
  /**
384
390
  * THE QUESTION: "how much is this material actually specified, and what gets
@@ -479,21 +485,12 @@ export declare class MaterialGraphClient {
479
485
  * {@link MaterialGraphClient.attributeValues} for the values it takes.
480
486
  */
481
487
  glossary(term: string, signal?: AbortSignal): Promise<GlossaryTermResponse>;
482
- /**
483
- * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
484
- * one OFFSET-paginated surface: read `meta.total` for the collection size and
485
- * `meta.hasMore` to keep walking. Instead use {@link MaterialGraphClient.brand}
486
- * for one brand's profile and trade signals.
487
- */
488
+ /** "Whose catalogs are in here, and how big is each?" — {@link listBrands}. */
488
489
  brands(options?: BrandsOptions, signal?: AbortSignal): Promise<BrandCatalogListPage>;
489
- /**
490
- * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
491
- * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
492
- * key means no value is held — never infer `false`. Instead use
493
- * {@link MaterialGraphClient.search} to find their materials; this describes the
494
- * company, not the catalog.
495
- */
490
+ /** "Who is this brand, and what do they claim?" — {@link brandProfile}. */
496
491
  brand(brandSlug: string, options?: BrandOptions, signal?: AbortSignal): Promise<BrandProfileResponse>;
492
+ /** "This maker's whole range, the way a fan deck reads" — {@link brandColourDeck}. */
493
+ brandColourDeck(brandSlug: string, signal?: AbortSignal): Promise<import("./fan-deck-types.js").BrandFanDeck>;
497
494
  /**
498
495
  * Upload one atomic source-material batch (POST `/api/v1/source-material`,
499
496
  * multipart). Pass a heterogeneous pile via `{ files }` (any mix of PDF,
@@ -566,14 +563,10 @@ export declare class MaterialGraphClient {
566
563
  * Poll {@link MaterialGraphClient.run} for the terminal state.
567
564
  */
568
565
  cancelRun(runId: RunId, signal?: AbortSignal): Promise<PublicRunResponse>;
569
- /**
570
- * Derive an "option B" scheme from a succeeded compose Run along one
571
- * constraint axis (POST `/api/v1/schemes/vary`). Deterministic and
572
- * synchronous — the response IS the hydrated scheme (no Run envelope), stamped
573
- * with axis + source provenance whose `notes` are the variation's receipts.
574
- * Requires the `mg:read` scope.
575
- */
566
+ /** Option B from a composed Run, along one axis. See {@link varySchemeCall}. */
576
567
  varyScheme(body: VarySchemesRequest, signal?: AbortSignal): Promise<VarySchemesResponse>;
568
+ /** Ranked candidate pools for up to 24 slots. See {@link slotPoolsCall}. */
569
+ slotPools(body: SlotPoolsRequest, signal?: AbortSignal): Promise<SlotPoolsResponse>;
577
570
  /**
578
571
  * Stream ordered Run events as an async iterator (GET
579
572
  * `/api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or the
package/client.js CHANGED
@@ -1,20 +1,25 @@
1
+ import { DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client-config.js";
1
2
  import { MaterialGraphError } from "./errors.js";
2
3
  import { HttpTransport } from "./http-transport.js";
3
4
  // Aliased to `operation` — the local name every call site here already used.
4
- import { resolveOperation as operation } from "./operations.js";
5
+ import { resolveOperation as operation, } from "./operations.js";
5
6
  import { cancelRun, capabilities as capabilitiesRun, createRun, drainRunEvents, getRun, getRunSchedule, getSourceMaterialBatch as getSourceMaterialBatchCall, listRuns, resumeRun, streamRunEvents, uploadSourceMaterial, } from "./runs.js";
6
- import { DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client-config.js";
7
7
  // Re-exported whole so `index.ts` and every existing importer keep their path.
8
8
  export * from "./client-config.js";
9
- import { browseQuery, createBrowseNamespace } from "./client-browse.js";
10
- import { suggestQuery } from "./suggest-types.js";
11
- import { matchStream as matchStreamCall } from "./match-stream.js";
12
- import { appendProjectEvent as appendProjectEventCall, createProject as createProjectCall, getProject as getProjectCall, listProjects as listProjectsCall, } from "./projects.js";
9
+ import { browseQuery, createBrowseNamespace, } from "./client-browse.js";
10
+ import { createExplorationNamespace, } from "./client-exploration.js";
11
+ import { createSourceMaterialCommentsNamespace, } from "./client-source-material-comments.js";
12
+ import { declaredFinishes, declaredProducts } from "./associations-client.js";
13
+ import { brandColourDeck, brandProfile, listBrands } from "./brands-client.js";
13
14
  import { CodesClient } from "./codes-client.js";
14
15
  import { ColoursClient } from "./colours-client.js";
15
- import { resolvePhrasesRequest, searchVocabularyRequest } from "./resolve-client.js";
16
- import { declaredFinishes, declaredProducts } from "./associations-client.js";
16
+ import { slotPools as slotPoolsCall, varyScheme as varySchemeCall, } from "./composition-client.js";
17
+ import { coordinate as coordinateCall } from "./coordinate-client.js";
18
+ import { matchStream as matchStreamCall } from "./match-stream.js";
19
+ import { appendProjectEvent as appendProjectEventCall, createProject as createProjectCall, getProject as getProjectCall, listProjects as listProjectsCall, } from "./projects.js";
20
+ import { resolvePhrasesRequest as resolvePhrases, searchVocabularyRequest, } from "./resolve-client.js";
17
21
  import { SCHEMA_RESOURCE_OPERATION_IDS } from "./schema-resource-types.js";
22
+ import { suggestQuery } from "./suggest-types.js";
18
23
  /**
19
24
  * The MaterialGraph client — every lens on the graph, on one object.
20
25
  *
@@ -54,14 +59,21 @@ import { SCHEMA_RESOURCE_OPERATION_IDS } from "./schema-resource-types.js";
54
59
  export class MaterialGraphClient {
55
60
  baseUrl;
56
61
  /**
57
- * THE QUESTION: "show me everything, ordered lightest / most vivid / most
58
- * muted." Pure colour scalars — no query, no relevance. `browse({ sort })` is
59
- * one page; `browse.pages({ sort })` walks them all, and only the walk can
60
- * support a corpus claim.
62
+ * THE QUESTION: "show me everything in this category / this brand, in a plain
63
+ * order." No query, no relevance, no ranking. `browse(params)` is one page;
64
+ * `browse.pages(params)` walks them all.
65
+ *
66
+ * `sort` is optional (`name` A→Z by default, `recent` for newest first, plus
67
+ * the five colour ramps), `types`/`brands`/`corpusScope` narrow it, and
68
+ * `count: true` returns the total so a listing can say how many things it is
69
+ * listing without walking every page. `results.length` remains a PAGE.
61
70
  *
62
71
  * Instead use {@link MaterialGraphClient.byColour} for materials NEAR a colour,
63
72
  * {@link MaterialGraphClient.search} when there is anything to rank against,
64
73
  * and `colours.profiles()` for a category's palette rather than its members.
74
+ *
75
+ * @example A category listing that can state its own size
76
+ * const page = await client.browse({ types: "tile_surfaces", count: true });
65
77
  */
66
78
  browse;
67
79
  /**
@@ -87,6 +99,14 @@ export class MaterialGraphClient {
87
99
  * likeness, never a declared colour specification.
88
100
  */
89
101
  colours;
102
+ /** Discover honest graph questions, then traverse one explicit operator. */
103
+ exploration;
104
+ /**
105
+ * THE QUESTION: "what did people say about this uploaded file, and record what
106
+ * they said." Names on these remarks are REPORTED, never verified — see
107
+ * {@link SourceMaterialCommentsNamespace}.
108
+ */
109
+ sourceMaterialComments;
90
110
  #transport;
91
111
  constructor(options) {
92
112
  if (!options.apiKey) {
@@ -107,15 +127,24 @@ export class MaterialGraphClient {
107
127
  get: (operationId, query, signal) => this.#get(operationId, query, signal),
108
128
  post: (operationId, body, signal) => this.#post(operationId, body, signal),
109
129
  });
130
+ this.exploration = createExplorationNamespace(this.#transport);
131
+ this.sourceMaterialComments = createSourceMaterialCommentsNamespace(this.#transport);
110
132
  }
111
133
  get lastRateLimit() {
112
134
  return this.#transport.lastRateLimit;
113
135
  }
114
136
  #get(operationId, query, signal, pathParams) {
115
- return this.#transport.requestJson(operation(operationId), { query, signal, pathParams });
137
+ return this.#transport.requestJson(operation(operationId), {
138
+ query,
139
+ signal,
140
+ pathParams,
141
+ });
116
142
  }
117
143
  #post(operationId, body, signal) {
118
- return this.#transport.requestJson(operation(operationId), { body, signal });
144
+ return this.#transport.requestJson(operation(operationId), {
145
+ body,
146
+ signal,
147
+ });
119
148
  }
120
149
  /**
121
150
  * THE QUESTION: "find me materials like this" — a RANKED answer over words, a
@@ -202,8 +231,7 @@ export class MaterialGraphClient {
202
231
  * {@link MaterialGraphClient.browse} to walk by lightness/vividness, and
203
232
  * {@link MaterialGraphClient.search} when colour is one signal among many.
204
233
  * Report matches as visual likeness, never a stated colour specification.
205
- * `family` gates the answer to ONE colorscope colour family when nearness
206
- * alone crosses a hue boundary; a gated page can be shorter than `limit`.
234
+ * `family` gates to ONE colorscope family (a gated page can be short); `brand` gates to named brands — one brand's nearest colour to another's.
207
235
  */
208
236
  byColour(params, signal) {
209
237
  return this.#get("search_by_colour", {
@@ -213,6 +241,7 @@ export class MaterialGraphClient {
213
241
  a: params.a,
214
242
  b: params.b,
215
243
  category: params.category,
244
+ brand: params.brand,
216
245
  family: params.family,
217
246
  limit: params.limit,
218
247
  }, signal);
@@ -307,7 +336,11 @@ export class MaterialGraphClient {
307
336
  if (ids.length > MAX_BATCH_IDS) {
308
337
  throw MaterialGraphError.unexpected(`MaterialGraphClient.materials(): at most ${MAX_BATCH_IDS} ids per call, received ${ids.length}. Split into multiple batches.`);
309
338
  }
310
- return this.#post("get_materials_batch_post", { ids, view: options?.view ?? "tile", idKind: options?.idKind ?? "variantId" }, signal);
339
+ return this.#post("get_materials_batch_post", {
340
+ ids,
341
+ view: options?.view ?? "tile",
342
+ idKind: options?.idKind ?? "variantId",
343
+ }, signal);
311
344
  }
312
345
  materialsCached(ids, options, signal) {
313
346
  if (ids.length > MAX_BATCH_IDS) {
@@ -388,38 +421,38 @@ export class MaterialGraphClient {
388
421
  searchVocabulary(query, options, signal) {
389
422
  return searchVocabularyRequest(this.#transport, query, options, signal);
390
423
  }
391
- /** "I have words — what do they mean in YOUR vocabulary?" — see {@link resolvePhrasesRequest}. */
392
- resolve(phrases, signal) {
393
- return resolvePhrasesRequest(this.#transport, phrases, signal);
424
+ /** "I have words — what do they mean, and what stands behind them?" — see {@link resolvePhrases}. */
425
+ resolve(phrases, scope) {
426
+ return resolvePhrases(this.#transport, phrases, scope);
394
427
  }
395
428
  /**
396
429
  * THE QUESTION: "which materials, from ANYONE, are ASTM E84 Class A AND
397
- * GREENGUARD-certified?" — hard specifications, no ranking, every brand at once
398
- * whatever each called the value in source.
430
+ * GREENGUARD-certified?" — hard specifications, no ranking, every brand at
431
+ * once whatever each called the value in source. The value-anchored,
432
+ * spec-only primitive (GET `/api/v1/search/specifications`).
399
433
  *
400
434
  * Instead use `search({ filters })` when the spec merely GATES a ranked
401
435
  * question, `codes.advisory()` when the real question is what a space demands,
402
- * and {@link MaterialGraphClient.specificationFacets} first if you do not know
403
- * the canonical tokens. Guessing one no longer returns a silently empty
404
- * answer: an unresolvable criterion means the query NEVER RAN, and the
405
- * response says so — read {@link BySpecResponse.unresolvedCriteria} before
406
- * treating an empty `matches` as "nothing qualifies".
407
- *
408
- * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
409
- * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
410
- *
411
- * Find materials by canonical specification across every brand — the
412
- * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
413
- * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
414
- * them, and each match carries per-criterion trust (`confidence` +
415
- * `sourceBasis`). Answers "which materials, from anyone, are ASTM E84 Class A
416
- * AND GREENGUARD-certified?" Build the strings with `specCriterion` /
417
- * `certificationProgrammeCriterion` (e.g.
418
- * `certificationProgrammeCriterion("GREENGUARD Gold")`); discover the queryable
419
- * vocabulary with {@link specificationFacets} and {@link attributeValues}.
436
+ * and {@link specificationFacets} first if you do not know the canonical
437
+ * tokens. Guessing one no longer returns a silently empty answer: an
438
+ * unresolvable criterion means the query NEVER RAN and the response says so —
439
+ * read {@link BySpecResponse.unresolvedCriteria} before treating an empty
440
+ * `matches` as "nothing qualifies". A PAGE, NOT A CORPUS, unusually so:
441
+ * `totalMatches` is the returned sample's size under `limit`, not a corpus
442
+ * count. Each `spec` is an `attribute:value` criterion built with
443
+ * `specCriterion` / `certificationProgrammeCriterion`; a variant must satisfy
444
+ * ALL of them, and each match carries per-criterion trust (`confidence` +
445
+ * `sourceBasis`). NUMBER-valued specs take BOUNDS, not values — pass
446
+ * `options.ranges` built with `specRangeCriterion("light_reflectance_value",
447
+ * { min: 60 })`; bounds are inclusive, at least one is required, and a
448
+ * non-numeric stored value is excluded rather than read as zero.
420
449
  */
421
450
  bySpec(spec, options, signal) {
422
- return this.#get("catalog_by_spec", { spec: Array.isArray(spec) ? spec : [spec], limit: options?.limit }, signal);
451
+ return this.#get("catalog_by_spec", {
452
+ spec: Array.isArray(spec) ? spec : [spec],
453
+ ...(options?.ranges?.length ? { specRange: options.ranges } : {}),
454
+ limit: options?.limit,
455
+ }, signal);
423
456
  }
424
457
  // ── Taxonomy + meta ──────────────────────────────────────────────────────────
425
458
  /**
@@ -440,19 +473,9 @@ export class MaterialGraphClient {
440
473
  return this.#get("whoami", undefined, signal);
441
474
  }
442
475
  // ── Pairings / demand ────────────────────────────────────────────────────────
443
- /**
444
- * THE QUESTION: "here is my whole scheme so far — what completes it?" The
445
- * multi-seed form of {@link MaterialGraphClient.pairings}: lift is summed across
446
- * seeds, so a companion suiting one pick ranks below one suiting several.
447
- * Instead use `pairings` for a single material and `colours.palette()` when the
448
- * scheme is COLOURS rather than materials. Check `unresolved` before presenting.
449
- *
450
- * Coordinate a palette of already-chosen materials (POST
451
- * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
452
- * colour swatches and an annotate-only `colourFit` (never reorders in v1).
453
- */
476
+ /** What completes a scheme already part-chosen. See {@link coordinateCall}. */
454
477
  coordinate(input, signal) {
455
- return this.#post("coordinate_materials", input, signal);
478
+ return coordinateCall(this.#transport, input, signal);
456
479
  }
457
480
  /**
458
481
  * THE QUESTION: "how much is this material actually specified, and what gets
@@ -575,26 +598,17 @@ export class MaterialGraphClient {
575
598
  return this.#get("explain_material_term", undefined, signal, { term });
576
599
  }
577
600
  // ── Brands ───────────────────────────────────────────────────────────────────
578
- /**
579
- * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
580
- * one OFFSET-paginated surface: read `meta.total` for the collection size and
581
- * `meta.hasMore` to keep walking. Instead use {@link MaterialGraphClient.brand}
582
- * for one brand's profile and trade signals.
583
- */
601
+ /** "Whose catalogs are in here, and how big is each?" — {@link listBrands}. */
584
602
  brands(options = {}, signal) {
585
- return this.#get("list_brands", { q: options.q, limit: options.limit, offset: options.offset }, signal);
603
+ return listBrands(this.#transport, options, signal);
586
604
  }
587
- /**
588
- * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
589
- * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
590
- * key means no value is held — never infer `false`. Instead use
591
- * {@link MaterialGraphClient.search} to find their materials; this describes the
592
- * company, not the catalog.
593
- */
605
+ /** "Who is this brand, and what do they claim?" — {@link brandProfile}. */
594
606
  brand(brandSlug, options, signal) {
595
- return this.#get("get_brand_profile", { q: options?.q }, signal, {
596
- brandSlug,
597
- });
607
+ return brandProfile(this.#transport, brandSlug, options, signal);
608
+ }
609
+ /** "This maker's whole range, the way a fan deck reads" — {@link brandColourDeck}. */
610
+ brandColourDeck(brandSlug, signal) {
611
+ return brandColourDeck(this.#transport, brandSlug, signal);
598
612
  }
599
613
  // ── Runs (the hosted agentic surface) ────────────────────────────────────────
600
614
  /**
@@ -687,15 +701,13 @@ export class MaterialGraphClient {
687
701
  cancelRun(runId, signal) {
688
702
  return cancelRun(this.#transport, runId, signal);
689
703
  }
690
- /**
691
- * Derive an "option B" scheme from a succeeded compose Run along one
692
- * constraint axis (POST `/api/v1/schemes/vary`). Deterministic and
693
- * synchronous — the response IS the hydrated scheme (no Run envelope), stamped
694
- * with axis + source provenance whose `notes` are the variation's receipts.
695
- * Requires the `mg:read` scope.
696
- */
704
+ /** Option B from a composed Run, along one axis. See {@link varySchemeCall}. */
697
705
  varyScheme(body, signal) {
698
- return this.#post("vary_schemes", body, signal);
706
+ return varySchemeCall(this.#transport, body, signal);
707
+ }
708
+ /** Ranked candidate pools for up to 24 slots. See {@link slotPoolsCall}. */
709
+ slotPools(body, signal) {
710
+ return slotPoolsCall(this.#transport, body, signal);
699
711
  }
700
712
  /**
701
713
  * Stream ordered Run events as an async iterator (GET
@@ -88,6 +88,43 @@ export interface ColourStatsGroup {
88
88
  productTypes: number;
89
89
  variants: number;
90
90
  }
91
+ /**
92
+ * One cell of the brand x category x colour-family matrix — the shape that
93
+ * answers questions no single dimension can, such as "how many whites does
94
+ * Benjamin Moore offer in paint".
95
+ */
96
+ export interface ColourStatsRow {
97
+ /** Opaque brand id — the join key for brand reads. Never parse it. */
98
+ brandId: string;
99
+ brandSlug: string;
100
+ brandName: string;
101
+ /**
102
+ * Product-type key. NULL means these variants carry NO accepted primary
103
+ * product type — a real population, kept so the matrix sums to the corpus,
104
+ * not an error and not a residual bucket.
105
+ */
106
+ categoryKey: string | null;
107
+ family: string;
108
+ /**
109
+ * Distinct variants at this (brand, category, family) carrying an ACCEPTED
110
+ * `colour_family`. Not every variant of the brand.
111
+ */
112
+ variants: number;
113
+ }
114
+ /** How to narrow the matrix. Passing ANY of these asks for `rows`. */
115
+ export interface ColourStatsParams {
116
+ /** `"brand,category"` — ask for the whole matrix, unscoped. */
117
+ groupBy?: string;
118
+ brandId?: string;
119
+ brandSlug?: string;
120
+ /**
121
+ * A product-type key, or the literal `"null"` for the unclassified
122
+ * population. Omitting it means every category — a different question.
123
+ */
124
+ categoryKey?: string;
125
+ family?: string;
126
+ limit?: number;
127
+ }
91
128
  /** Where each section came from, how fresh it is, and what it does NOT answer. */
92
129
  export interface ColourStatsBasis {
93
130
  corpus: string;
@@ -95,11 +132,32 @@ export interface ColourStatsBasis {
95
132
  groups: string;
96
133
  groupsRefreshedAt: string | null;
97
134
  groupsVariantCount: number;
135
+ /**
136
+ * Where `rows` came from, its freshness, and what it EXCLUDES — present only
137
+ * when `rows` is. Read it: the matrix deliberately omits Samplize, which is a
138
+ * sampling channel printing other manufacturers' colours rather than a maker.
139
+ */
140
+ rows?: string;
98
141
  omissions: string[];
99
142
  }
100
143
  export interface ColourStatsResponse {
101
144
  corpus: ColourStatsCorpus;
102
145
  families: ColourStatsFamily[];
103
146
  groups: ColourStatsGroup[];
147
+ /**
148
+ * The brand x category x colour-family matrix. Present ONLY when you asked
149
+ * for it; ABSENT means nobody asked, never that the matrix is empty.
150
+ *
151
+ * `families` above is NOT scoped by the same parameters and stays
152
+ * platform-wide under all of them. Scoped counts live here.
153
+ */
154
+ rows?: ColourStatsRow[];
155
+ /**
156
+ * Rows matching your query BEFORE the response cap — the honest total.
157
+ * A shortfall against `rows.length` means `rows` is a prefix.
158
+ */
159
+ rowsMatched?: number;
160
+ /** TRUE when the matrix exceeded the cap and `rows` is a prefix. */
161
+ rowsTruncated?: boolean;
104
162
  basis: ColourStatsBasis;
105
163
  }
@@ -32,7 +32,7 @@
32
32
  * FROM PRODUCT IMAGERY, not declared by the manufacturer. Report visual
33
33
  * likeness, never a stated colour specification.
34
34
  */
35
- import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsResponse } from "./colour-aggregate-types.js";
35
+ import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsParams, ColourStatsResponse } from "./colour-aggregate-types.js";
36
36
  import type { QueryParams } from "./http-transport.js";
37
37
  import type { SdkOperationId } from "./operations.js";
38
38
  import type { SearchGradientInput, SearchGradientResponse, SearchPaletteInput, SearchPaletteResponse } from "./palette-search-types.js";
@@ -87,8 +87,14 @@ export declare class ColoursClient {
87
87
  * coverage per product-type group. `basis` states the source and freshness of
88
88
  * each section plus `basis.omissions` — what this call deliberately does not
89
89
  * answer, listed rather than silently absent.
90
+ *
91
+ * Pass `{ groupBy: "brand,category" }` for the whole brand x category x
92
+ * family matrix in one call, or a `brandSlug`/`categoryKey` scope for a slice
93
+ * of it — either way it arrives as `rows`, each row carrying `brandId` and
94
+ * `brandSlug` to join on. Without any of those parameters the payload is the
95
+ * platform-wide one and `rows` is absent. `families` is never scoped by them.
90
96
  */
91
- stats(signal?: AbortSignal): Promise<ColourStatsResponse>;
97
+ stats(params?: ColourStatsParams, signal?: AbortSignal): Promise<ColourStatsResponse>;
92
98
  /**
93
99
  * THE QUESTION: "what goes with this WHOLE PALETTE?" — N colours at once, at
94
100
  * stated proportions, not one colour repeated (POST `/api/v1/search/palettes`).
package/colours-client.js CHANGED
@@ -53,9 +53,22 @@ export class ColoursClient {
53
53
  * coverage per product-type group. `basis` states the source and freshness of
54
54
  * each section plus `basis.omissions` — what this call deliberately does not
55
55
  * answer, listed rather than silently absent.
56
+ *
57
+ * Pass `{ groupBy: "brand,category" }` for the whole brand x category x
58
+ * family matrix in one call, or a `brandSlug`/`categoryKey` scope for a slice
59
+ * of it — either way it arrives as `rows`, each row carrying `brandId` and
60
+ * `brandSlug` to join on. Without any of those parameters the payload is the
61
+ * platform-wide one and `rows` is absent. `families` is never scoped by them.
56
62
  */
57
- stats(signal) {
58
- return this.#transport.get("get_colour_stats", undefined, signal);
63
+ stats(params = {}, signal) {
64
+ return this.#transport.get("get_colour_stats", {
65
+ groupBy: params.groupBy,
66
+ brandId: params.brandId,
67
+ brandSlug: params.brandSlug,
68
+ categoryKey: params.categoryKey,
69
+ family: params.family,
70
+ limit: params.limit,
71
+ }, signal);
59
72
  }
60
73
  /**
61
74
  * THE QUESTION: "what goes with this WHOLE PALETTE?" — N colours at once, at