@instruments/materialgraph 0.26.0 → 0.27.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 (113) hide show
  1. package/association-types.d.ts +18 -7
  2. package/attribution-types.d.ts +2 -2
  3. package/brand-types.d.ts +8 -0
  4. package/brands-client.d.ts +5 -36
  5. package/brands-client.js +7 -33
  6. package/by-colour-types.d.ts +40 -13
  7. package/by-spec-types.d.ts +0 -67
  8. package/by-spec-types.js +7 -5
  9. package/catalog-discovery-types.d.ts +1 -16
  10. package/catalogue-client.d.ts +12 -31
  11. package/catalogue-client.js +10 -46
  12. package/catalogue-types.d.ts +40 -0
  13. package/categories-client.d.ts +63 -0
  14. package/categories-client.js +48 -0
  15. package/category-types.d.ts +114 -0
  16. package/client-config.d.ts +2 -4
  17. package/client-config.js +2 -4
  18. package/client-vocabulary.d.ts +2 -4
  19. package/client-vocabulary.js +11 -9
  20. package/client.d.ts +103 -203
  21. package/client.js +126 -251
  22. package/codes-types.d.ts +21 -0
  23. package/colours-client.d.ts +4 -9
  24. package/colours-client.js +0 -8
  25. package/errors.d.ts +1 -1
  26. package/errors.js +1 -0
  27. package/identify-types.d.ts +49 -39
  28. package/identify-types.js +5 -3
  29. package/index.d.ts +11 -21
  30. package/index.js +4 -4
  31. package/list-variant-facets-types.d.ts +60 -0
  32. package/list-variants-types.d.ts +142 -0
  33. package/list-variants-types.js +12 -0
  34. package/match-stream-types.d.ts +13 -0
  35. package/match-stream-types.js +2 -0
  36. package/match-stream.js +12 -8
  37. package/match-types.d.ts +24 -0
  38. package/material-asset-types.d.ts +69 -49
  39. package/material-block-types.d.ts +2 -0
  40. package/material-composition-client.d.ts +21 -0
  41. package/material-composition-client.js +13 -0
  42. package/material-composition-types.d.ts +403 -0
  43. package/material-query-plan-types.d.ts +57 -141
  44. package/material-resources-client.d.ts +9 -7
  45. package/material-resources-client.js +19 -4
  46. package/material-types.d.ts +6 -14
  47. package/operations.d.ts +1 -1
  48. package/operations.js +20 -117
  49. package/package.json +5 -1
  50. package/palette-search-types.d.ts +59 -24
  51. package/query-understanding-types.d.ts +27 -5
  52. package/resolve-client.d.ts +6 -10
  53. package/resolve-client.js +5 -7
  54. package/response-schemas.d.ts +20 -0
  55. package/response-schemas.generated.d.ts +5 -0
  56. package/response-schemas.generated.js +160 -0
  57. package/response-schemas.js +113 -0
  58. package/run-types.d.ts +12 -93
  59. package/runs.d.ts +1 -37
  60. package/runs.js +2 -79
  61. package/schema-resource-types.d.ts +14 -1
  62. package/search-card-types.d.ts +22 -0
  63. package/search-index-plan-types.d.ts +70 -0
  64. package/search-refinement-types.d.ts +8 -38
  65. package/search-refinement-types.js +1 -0
  66. package/search-request-types.d.ts +117 -200
  67. package/search-response-receipt-types.d.ts +12 -268
  68. package/search-result-validation.d.ts +0 -16
  69. package/search-result-validation.js +22 -180
  70. package/spec-facet-types.d.ts +3 -8
  71. package/spec-types.d.ts +17 -10
  72. package/spec-types.js +8 -7
  73. package/specifications.d.ts +13 -55
  74. package/specifications.js +29 -73
  75. package/suggest-types.d.ts +4 -10
  76. package/suggest-types.js +1 -1
  77. package/types.d.ts +135 -338
  78. package/webmcp.d.ts +1 -1
  79. package/webmcp.generated.js +10 -94
  80. package/brand-most-sampled-types.d.ts +0 -33
  81. package/browse-types.d.ts +0 -236
  82. package/client-browse.d.ts +0 -78
  83. package/client-browse.js +0 -71
  84. package/client-exploration.d.ts +0 -9
  85. package/client-exploration.js +0 -5
  86. package/compare-types.d.ts +0 -89
  87. package/composition-client.d.ts +0 -33
  88. package/composition-client.js +0 -28
  89. package/composition-types.d.ts +0 -455
  90. package/composition-types.js +0 -10
  91. package/correction-types.d.ts +0 -88
  92. package/exploration-types.d.ts +0 -118
  93. package/exploration-types.js +0 -1
  94. package/fan-deck-types.d.ts +0 -42
  95. package/fan-deck-types.js +0 -9
  96. package/match-rejection-types.d.ts +0 -43
  97. package/match-rejection-types.js +0 -7
  98. package/palette-completion-types.d.ts +0 -115
  99. package/palette-completion-types.js +0 -1
  100. package/render-assets-types.d.ts +0 -65
  101. package/render-assets-types.js +0 -1
  102. package/search-collapse-types.d.ts +0 -69
  103. package/search-collapse-types.js +0 -1
  104. package/search-mood-types.d.ts +0 -6
  105. package/search-mood-types.js +0 -1
  106. package/search-window-types.d.ts +0 -25
  107. package/search-window-types.js +0 -1
  108. package/slot-pool-types.d.ts +0 -372
  109. package/slot-pool-types.js +0 -1
  110. /package/{brand-most-sampled-types.js → category-types.js} +0 -0
  111. /package/{browse-types.js → list-variant-facets-types.js} +0 -0
  112. /package/{compare-types.js → material-composition-types.js} +0 -0
  113. /package/{correction-types.js → search-index-plan-types.js} +0 -0
package/client.js CHANGED
@@ -1,23 +1,22 @@
1
1
  import * as assoc from "./associations-client.js";
2
- import { brandColourDeck, brandProfile, listBrands } from "./brands-client.js";
2
+ import { listBrands } from "./brands-client.js";
3
3
  import { CatalogueClient } from "./catalogue-client.js";
4
- import { browseQuery, createBrowseNamespace } from "./client-browse.js";
5
- import { DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client-config.js";
6
- import { createExplorationNamespace } from "./client-exploration.js";
4
+ import { CategoriesClient } from "./categories-client.js";
5
+ import { DEFAULT_BASE_URL } from "./client-config.js";
7
6
  import { createSourceMaterialCommentsNamespace } from "./client-source-material-comments.js";
8
- import { colourFacets as colourFacetsCall, resolveAttribute as resolveAttributeCall, resolveAttributeValue as resolveAttributeValueCall, resolveColour as resolveColourCall, suggest as suggestCall, suggestColour as suggestColourCall, } from "./client-vocabulary.js";
7
+ import { listAttributes as listAttributesCall, resolveColour as resolveColourCall, suggest as suggestCall, suggestColour as suggestColourCall, } from "./client-vocabulary.js";
9
8
  import { CodesClient } from "./codes-client.js";
10
9
  import { ColoursClient } from "./colours-client.js";
11
- import { slotPools as slotPoolsCall, varyScheme as varySchemeCall, } from "./composition-client.js";
12
10
  import { coordinate as coordinateCall } from "./coordinate-client.js";
13
11
  import { MaterialGraphError } from "./errors.js";
14
12
  import { HttpTransport } from "./http-transport.js";
15
13
  import { matchStream as matchStreamCall } from "./match-stream.js";
16
- import { materialAssets as materialAssetsCall, materialRenderAssets as materialRenderAssetsCall, materialBlocks as materialBlocksCall, materialDocuments as materialDocumentsCall, } from "./material-resources-client.js";
14
+ import { compositionReport as compositionReportCall } from "./material-composition-client.js";
15
+ import { listVariantAssets as listVariantAssetsCall, materialBlocks as materialBlocksCall, materialDocuments as materialDocumentsCall, } from "./material-resources-client.js";
17
16
  // Aliased to `operation` — the local name every call site here already used.
18
17
  import { resolveOperation as operation } from "./operations.js";
19
18
  import { resolvePhrasesRequest as resolvePhrases, searchVocabularyRequest, } from "./resolve-client.js";
20
- import { cancelRun, capabilities as capabilitiesRun, createRun, drainRunEvents, getRun, getSourceMaterialBatch as getSourceMaterialBatchCall, listRuns, resumeRun, streamRunEvents, uploadSourceMaterial, } from "./runs.js";
19
+ import { createRun, drainRunEvents, getRun, getSourceMaterialBatch as getSourceMaterialBatchCall, streamRunEvents, uploadSourceMaterial, } from "./runs.js";
21
20
  import { SCHEMA_RESOURCE_OPERATION_IDS } from "./schema-resource-types.js";
22
21
  // Re-exported whole so `index.ts` and every existing importer keep their path.
23
22
  export * from "./client-config.js";
@@ -31,13 +30,13 @@ export * from "./client-config.js";
31
30
  * | --- | --- |
32
31
  * | "find me something like this / matching these words" | {@link MaterialGraphClient.search} |
33
32
  * | "what's IN this photo?" | {@link MaterialGraphClient.match} / {@link MaterialGraphClient.matchStream} |
34
- * | "which materials, from anyone, meet this hard spec?" | {@link MaterialGraphClient.bySpec} |
33
+ * | "which materials, from anyone, meet this hard spec?" | {@link MaterialGraphClient.listVariants} |
35
34
  * | "what's near this colour / this palette / this gradient?" | {@link MaterialGraphClient.byColour}, `colours.palette()`, `colours.gradient()` |
36
- * | "what goes with THIS material?" | {@link MaterialGraphClient.pairings}, {@link MaterialGraphClient.siblings}, {@link MaterialGraphClient.coordinate} |
35
+ * | "what goes with THIS material?" | {@link MaterialGraphClient.pairings}, {@link MaterialGraphClient.listVariants} (same product, other colourways), {@link MaterialGraphClient.coordinate} |
37
36
  * | "what does the manufacturer say is AVAILABLE on this?" | {@link MaterialGraphClient.associatedFinishes}, {@link MaterialGraphClient.associatedProducts} |
38
37
  * | "how much is this specified?" | {@link MaterialGraphClient.demand}, {@link MaterialGraphClient.demandBatch} |
39
38
  * | "is this allowed in an A-2 corridor?" | `codes.*` |
40
- * | "what can I even ask for?" | {@link MaterialGraphClient.specificationFacets}, {@link MaterialGraphClient.attributeValues}, {@link MaterialGraphClient.taxonomy}, {@link MaterialGraphClient.colourFacets}, {@link MaterialGraphClient.glossary} |
39
+ * | "what can I even ask for?" | {@link MaterialGraphClient.specificationFacets}, `categories.list()` |
41
40
  * | "I have words — what do they mean in YOUR vocabulary?" | {@link MaterialGraphClient.resolve} |
42
41
  * | "do the whole job for me, async" | {@link MaterialGraphClient.createRun} + `mg:compose` |
43
42
  *
@@ -59,30 +58,26 @@ export * from "./client-config.js";
59
58
  export class MaterialGraphClient extends CatalogueClient {
60
59
  baseUrl;
61
60
  /**
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.
61
+ * THE QUESTION: "what kinds of thing exist, and what are their keys?"
62
+ * `categories.list()` pages the tree (roots, one node's children, or a
63
+ * name/meaning search), and `categories.get(id)` reads one node in full:
64
+ * its place, the names that find it, and the canonical taxonomy concepts it
65
+ * relates to. The source of every category id the other lenses accept
66
+ * (`search({ category })`, `listVariants({ filters })`, `colours.profiles()`).
65
67
  *
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.
70
- *
71
- * Instead use {@link MaterialGraphClient.byColour} for materials NEAR a colour,
72
- * {@link MaterialGraphClient.search} when there is anything to rank against,
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 });
68
+ * A category id is a TREE KEY, never a canonical `product_category` value:
69
+ * the two look alike and are not interchangeable. Discover ids here, never
70
+ * hand-write them. Replaces the retired `taxonomy()` (MG-2226), with no
71
+ * shim.
77
72
  */
78
- browse;
73
+ categories;
79
74
  /**
80
75
  * THE QUESTION: "is this material allowed HERE?" — a jurisdiction + occupancy +
81
76
  * finish location, not a property of the material alone. `codes.context()`
82
77
  * discovers the vocabulary, `codes.requirements()` states a space,
83
78
  * `codes.advisory()` badges candidates.
84
79
  *
85
- * Instead use {@link MaterialGraphClient.bySpec} to find materials by a
80
+ * Instead use {@link MaterialGraphClient.listVariants} to find materials by a
86
81
  * classification you already know you need; codes is what tells you WHICH one
87
82
  * the space demands. ADVISORY ONLY: model code as adopted, never a
88
83
  * determination for an authority having jurisdiction.
@@ -92,15 +87,14 @@ export class MaterialGraphClient extends CatalogueClient {
92
87
  * THE QUESTION: "what colours exist here?" (`profiles`, `stats`) and "what
93
88
  * matches this whole SET of colours?" (`palette`, `gradient`).
94
89
  *
95
- * Instead use {@link MaterialGraphClient.byColour} for ONE colour,
96
- * {@link MaterialGraphClient.browse} to walk by a colour scalar, and
97
- * {@link MaterialGraphClient.colourFacets} for the colour VOCABULARY rather
98
- * than values. Colour throughout is derived from product imagery — visual
99
- * likeness, never a declared colour specification.
90
+ * Instead use {@link MaterialGraphClient.byColour} for ONE colour and
91
+ * {@link MaterialGraphClient.listVariants} to page through a colour's family.
92
+ * Colour is usually derived from product imagery, which is visual likeness,
93
+ * not a colour specification. A maker's declared colour, such as a paint's
94
+ * published hex, wins where MaterialGraph holds one, and each value's
95
+ * source basis says which.
100
96
  */
101
97
  colours;
102
- /** Discover honest graph questions, then traverse one explicit operator. */
103
- exploration;
104
98
  /**
105
99
  * THE QUESTION: "what did people say about this uploaded file, and record what
106
100
  * they said." Names on these remarks are REPORTED, never verified — see
@@ -120,7 +114,9 @@ export class MaterialGraphClient extends CatalogueClient {
120
114
  super(transport);
121
115
  this.#transport = transport;
122
116
  this.baseUrl = transport.baseUrl;
123
- this.browse = createBrowseNamespace(async (params, signal) => await this.#get("search_browse_by_colour", browseQuery(params), signal));
117
+ this.categories = new CategoriesClient({
118
+ get: async (operationId, query, signal, pathParams) => await this.#get(operationId, query, signal, pathParams),
119
+ });
124
120
  this.codes = new CodesClient({
125
121
  get: async (operationId, query, signal) => await this.#get(operationId, query, signal),
126
122
  post: async (operationId, body, signal) => await this.#post(operationId, body, signal),
@@ -129,7 +125,6 @@ export class MaterialGraphClient extends CatalogueClient {
129
125
  get: async (operationId, query, signal) => await this.#get(operationId, query, signal),
130
126
  post: async (operationId, body, signal) => await this.#post(operationId, body, signal),
131
127
  });
132
- this.exploration = createExplorationNamespace(this.#transport);
133
128
  this.sourceMaterialComments = createSourceMaterialCommentsNamespace(this.#transport);
134
129
  }
135
130
  get lastRateLimit() {
@@ -149,21 +144,6 @@ export class MaterialGraphClient extends CatalogueClient {
149
144
  signal,
150
145
  });
151
146
  }
152
- /**
153
- * THE QUESTION: "find me materials like this" — a RANKED answer over words, a
154
- * seed image, colours and a mood, optionally gated by hard specs.
155
- *
156
- * Instead use {@link MaterialGraphClient.bySpec} when the spec IS the question
157
- * (ranking would be noise), {@link MaterialGraphClient.byColour} /
158
- * `colours.palette()` for visual colour intent (`text` matches WORDS — "sage
159
- * green" finds materials that SAY sage, not ones that look it),
160
- * {@link MaterialGraphClient.match} for a photo, {@link MaterialGraphClient.pairings}
161
- * for what goes WITH something, {@link MaterialGraphClient.suggest} for typeahead.
162
- *
163
- * The full POST search primitive. `input.filters` layers typed hard
164
- * specification gates onto ranked colour/visual/lexical/seed search. Build
165
- * them with `certificationProgramme`, `specTerm`, or `hasSpec`.
166
- */
167
147
  async search(input, signal) {
168
148
  return await this.#post("search_materials_post", input, signal);
169
149
  }
@@ -171,12 +151,6 @@ export class MaterialGraphClient extends CatalogueClient {
171
151
  async planQuery(query, signal) {
172
152
  return await this.#post("plan_material_query", { query }, signal);
173
153
  }
174
- /**
175
- * The same question as {@link MaterialGraphClient.search}; the difference is
176
- * transport — a cacheable, link-shareable GET for queries that must survive in
177
- * a URL. `filters`, `preferences`, `verdicts`, image bytes and `weights` have
178
- * no query-string form, so anything using those must go through `search`.
179
- */
180
154
  async searchGet(params, signal) {
181
155
  return await this.#get("search_materials_get", { ...params }, signal);
182
156
  }
@@ -229,7 +203,7 @@ export class MaterialGraphClient extends CatalogueClient {
229
203
  * Instead use `colours.palette()` for N colours at stated proportions (a
230
204
  * different ranking, not a wrapper), `colours.gradient()` for a transition,
231
205
  * {@link MaterialGraphClient.resolveColour} first if you hold a NAME,
232
- * {@link MaterialGraphClient.browse} to walk by lightness/vividness, and
206
+ * {@link MaterialGraphClient.listVariants} to page through a category or a brand, and
233
207
  * {@link MaterialGraphClient.search} when colour is one signal among many.
234
208
  * Report matches as visual likeness, never a stated colour specification.
235
209
  * `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.
@@ -249,6 +223,7 @@ export class MaterialGraphClient extends CatalogueClient {
249
223
  maxColourDistance: params.maxColourDistance,
250
224
  order: params.order,
251
225
  q: params.q,
226
+ variantId: params.variantId,
252
227
  }, signal);
253
228
  }
254
229
  // ── Materials ──────────────────────────────────────────────────────────────
@@ -257,35 +232,34 @@ export class MaterialGraphClient extends CatalogueClient {
257
232
  * `/api/v1/materials/{variantId}`).
258
233
  *
259
234
  * The hinge of the API: every relationship lens
260
- * ({@link MaterialGraphClient.siblings}, {@link MaterialGraphClient.pairings},
235
+ * ({@link MaterialGraphClient.listVariants}, {@link MaterialGraphClient.pairings},
261
236
  * {@link MaterialGraphClient.demand}, `codes.advisory()`) hangs off a
262
- * `variantId`. Instead use {@link MaterialGraphClient.materials} for many ids,
263
- * {@link MaterialGraphClient.materialBlocks} to tell "no value" from "not
264
- * checked", {@link MaterialGraphClient.search} when you have no id yet.
237
+ * `variantId`. Instead use {@link MaterialGraphClient.listVariants} for many
238
+ * ids, or for the other colourways of the same product (MG-2222 retired
239
+ * this route's `?include=siblings`; ask `listVariants` with a `product in`
240
+ * filter instead), {@link MaterialGraphClient.materialBlocks} to tell "no
241
+ * value" from "not checked", {@link MaterialGraphClient.search} when you
242
+ * have no id yet.
265
243
  *
266
244
  * @param variantId Opaque — pass back exactly what you were given.
267
245
  */
268
- async material(variantId, options, signal) {
269
- return await this.#get("get_material", options?.includeSiblings ? { include: "siblings" } : undefined, signal, { variantId });
270
- }
271
- /**
272
- * THE QUESTION: "what other colourways and finishes does this product come in?"
273
- * — the SAME product, different variants.
274
- *
275
- * Instead use {@link MaterialGraphClient.pairings} for a DIFFERENT product that
276
- * goes with this one, and `search({ similarTo })` for one that LOOKS like it.
277
- */
278
- async siblings(variantId, signal) {
279
- return await this.#get("get_material_siblings", undefined, signal, { variantId });
246
+ async material(variantId, signal) {
247
+ return await this.#get("get_material", undefined, signal, { variantId });
280
248
  }
281
249
  async materialBlocks(variantId, signal) {
282
250
  return await materialBlocksCall(this.#transport, variantId, signal);
283
251
  }
284
- async materialAssets(variantId, signal) {
285
- return await materialAssetsCall(this.#transport, variantId, signal);
252
+ /**
253
+ * THE QUESTION: "which product types can composition be answered for yet?"
254
+ *
255
+ * Coverage per product type, in Live SKUs as well as tuples. Read the SKU
256
+ * figure: one tuple can carry a hundred thousand SKUs or one.
257
+ */
258
+ async compositionReport(signal) {
259
+ return await compositionReportCall(this.#transport, signal);
286
260
  }
287
- async materialRenderAssets(variantId, signal) {
288
- return await materialRenderAssetsCall(this.#transport, variantId, signal);
261
+ async listVariantAssets(variantId, options, signal) {
262
+ return await listVariantAssetsCall(this.#transport, variantId, options, signal);
289
263
  }
290
264
  async materialDocuments(variantId, options, signal) {
291
265
  return await materialDocumentsCall(this.#transport, variantId, options, signal);
@@ -294,8 +268,7 @@ export class MaterialGraphClient extends CatalogueClient {
294
268
  * THE QUESTION: "what does the schema DEFINE?" — the governed static registry.
295
269
  *
296
270
  * Definitions, not data: use {@link MaterialGraphClient.specificationFacets} for
297
- * what the corpus actually holds and {@link MaterialGraphClient.glossary} to
298
- * explain one term.
271
+ * what the corpus actually holds.
299
272
  */
300
273
  async schema(resource, signal) {
301
274
  return await this.#get(SCHEMA_RESOURCE_OPERATION_IDS[resource], undefined, signal);
@@ -305,9 +278,9 @@ export class MaterialGraphClient extends CatalogueClient {
305
278
  * off one resolved material (GET `/api/v1/materials/{variantId}/pairings`).
306
279
  *
307
280
  * Instead use {@link MaterialGraphClient.coordinate} for a whole palette of
308
- * picks, {@link MaterialGraphClient.siblings} for the same product in another
309
- * colour, and `search({ similarTo })` for a LIKE-THIS substitute — similar and
310
- * complementary are opposite questions.
281
+ * picks, {@link MaterialGraphClient.listVariants} for the same product in
282
+ * another colour, and `search({ similarTo })` for a LIKE-THIS substitute —
283
+ * similar and complementary are opposite questions.
311
284
  *
312
285
  * READ `item.source` BEFORE PHRASING: the rail mixes three tiers of evidence —
313
286
  * `cospec` (real orders), `considered` (board activity), `colour` (palette
@@ -319,10 +292,6 @@ export class MaterialGraphClient extends CatalogueClient {
319
292
  variantId,
320
293
  });
321
294
  }
322
- /** Find bounded, exact-spec substitutes for one opaque variant id. */
323
- async findSubstitutes(variantId, options = {}, signal) {
324
- return await this.#post("find_material_substitutes", options, signal, { variantId });
325
- }
326
295
  /** "What finishes does the manufacturer declare for THIS product?" — {@link declaredFinishes}. */
327
296
  async associatedFinishes(variantId, options, signal) {
328
297
  return await assoc.declaredFinishes(this.#transport, variantId, options, signal);
@@ -334,29 +303,50 @@ export class MaterialGraphClient extends CatalogueClient {
334
303
  async applicability(options, signal) {
335
304
  return await assoc.applicability(this.#transport, options, signal);
336
305
  }
337
- async materials(ids, options, signal) {
338
- if (ids.length > MAX_BATCH_IDS) {
339
- throw MaterialGraphError.unexpected(`MaterialGraphClient.materials(): at most ${MAX_BATCH_IDS} ids per call, received ${ids.length}. Split into multiple batches.`);
340
- }
341
- return await this.#post("get_materials_batch_post", {
342
- idKind: options?.idKind ?? "variantId",
343
- ids,
344
- view: options?.view ?? "tile",
345
- }, signal);
346
- }
347
- async materialsCached(ids, options, signal) {
348
- if (ids.length > MAX_BATCH_IDS) {
349
- throw MaterialGraphError.unexpected(`MaterialGraphClient.materialsCached(): at most ${MAX_BATCH_IDS} ids per call, received ${ids.length}. Split into multiple batches.`);
350
- }
351
- return await this.#get("get_materials_batch_get", {
352
- idKind: options?.idKind ?? "variantId",
353
- ids: ids.join(","),
354
- view: options?.view ?? "tile",
306
+ /**
307
+ * THE QUESTION: "eligible variants matching these hard filters, ranked and
308
+ * cursor-paged" (GET `/api/v1/variants`, MG-2222) — the one read behind
309
+ * `list_variants`, hydrating ids you already hold (`{terms: [{field:
310
+ * "variant", op: "in", values: [...]}]}`, up to 100), the other colourways
311
+ * of a product (`{terms: [{field: "product", op: "in", values:
312
+ * [productId]}, {field: "variant", op: "not_in", values: [variantId]}]}` —
313
+ * `filters` is kind-grouped, `{terms, numbers, presence}`, every condition
314
+ * across all three ANDed), or a category/maker listing. An explicit
315
+ * `variant in` id that does not resolve to an eligible variant is
316
+ * reported in `unresolvedVariantIds`, never silently dropped from the
317
+ * page.
318
+ *
319
+ * `filters`/`optionalFilters`/`orderBy` are sent as URL-encoded JSON;
320
+ * `attributes`/`include` as comma-separated lists — the server validates,
321
+ * this method does not duplicate that check.
322
+ */
323
+ async listVariants(input, signal) {
324
+ return await this.#get("list_variants", {
325
+ attributes: input.attributes?.length
326
+ ? input.attributes.join(",")
327
+ : undefined,
328
+ cursor: input.cursor,
329
+ filters: input.filters ? JSON.stringify(input.filters) : undefined,
330
+ include: input.include?.length ? input.include.join(",") : undefined,
331
+ limit: input.limit,
332
+ name: input.name,
333
+ optionalFilters: input.optionalFilters
334
+ ? JSON.stringify(input.optionalFilters)
335
+ : undefined,
336
+ orderBy: input.orderBy?.length
337
+ ? JSON.stringify(input.orderBy)
338
+ : undefined,
355
339
  }, signal);
356
340
  }
357
- /** Vocabulary families and counts; use `colours.profiles()` for a material palette. */
358
- async colourFacets(signal) {
359
- return await colourFacetsCall(this.#transport, signal);
341
+ /**
342
+ * THE QUESTION: "which values do the variants matching these filters carry,
343
+ * and how many of each" (POST `/api/v1/variants/facets`, MG-2294) — the
344
+ * options and counts a filter UI shows beside a {@link
345
+ * MaterialGraphClient.listVariants} page. `filters` is the same tree
346
+ * `listVariants()` takes; the server validates both.
347
+ */
348
+ async listVariantFacets(input, signal) {
349
+ return await this.#post("list_variant_facets", input, signal);
360
350
  }
361
351
  /** Resolve a colour name to its hex, and the named paint's catalogue variant. */
362
352
  async resolveColour(name, signal, options) {
@@ -371,13 +361,17 @@ export class MaterialGraphClient extends CatalogueClient {
371
361
  return await suggestCall(this.#transport, q, options, signal);
372
362
  }
373
363
  // ── Catalog ──────────────────────────────────────────────────────────────────
374
- /** Resolve free text to a canonical attribute id. */
375
- async resolveAttribute(q, options, signal) {
376
- return await resolveAttributeCall(this.#transport, q, options, signal);
377
- }
378
- /** Resolve a value under a canonical attribute id; inspect `recommendedAction` for ambiguity. */
379
- async resolveAttributeValue(attributeId, query, options, signal) {
380
- return await resolveAttributeValueCall(this.#transport, attributeId, query, options, signal);
364
+ /**
365
+ * THE QUESTION: "which attribute means this phrase?" or "what could a
366
+ * <category> be specified by?" (GET `/api/v1/catalog/attributes`). One page of
367
+ * attribute definitions, browsed or found by id, name, misspelling or
368
+ * meaning, scoped by `categoryIds`, `blockIds` and `filterableOnly`. Pass an
369
+ * item's `id` with one of its `filterOps` to
370
+ * {@link MaterialGraphClient.listVariants}; follow `pageInfo.endCursor` for
371
+ * the next page.
372
+ */
373
+ async listAttributes(options, signal) {
374
+ return await listAttributesCall(this.#transport, options, signal);
381
375
  }
382
376
  /** "Is this phrase an attribute or a value?" — see {@link searchVocabularyRequest}. */
383
377
  async searchVocabulary(query, options, signal) {
@@ -387,45 +381,16 @@ export class MaterialGraphClient extends CatalogueClient {
387
381
  async resolve(phrases, scope) {
388
382
  return await resolvePhrases(this.#transport, phrases, scope);
389
383
  }
390
- /**
391
- * THE QUESTION: "which materials, from ANYONE, are ASTM E84 Class A AND
392
- * GREENGUARD-certified?" — hard specifications, no ranking, every brand at
393
- * once whatever each called the value in source. The value-anchored,
394
- * spec-only primitive (GET `/api/v1/search/specifications`).
395
- *
396
- * Instead use `search({ filters })` when the spec merely GATES a ranked
397
- * question, `codes.advisory()` when the real question is what a space demands,
398
- * and {@link specificationFacets} first if you do not know the canonical
399
- * tokens. Guessing one no longer returns a silently empty answer: an
400
- * unresolvable criterion means the query NEVER RAN and the response says so —
401
- * read {@link BySpecResponse.unresolvedCriteria} before treating an empty
402
- * `matches` as "nothing qualifies". A PAGE, NOT A CORPUS, unusually so:
403
- * `totalMatches` is the returned sample's size under `limit`, not a corpus
404
- * count. Each `spec` is an `attribute:value` criterion built with
405
- * `specCriterion` / `certificationProgrammeCriterion`; a variant must satisfy
406
- * ALL of them, and each match carries per-criterion trust (`confidence` +
407
- * `sourceBasis`). NUMBER-valued specs take BOUNDS, not values — pass
408
- * `options.ranges` built with `specRangeCriterion("light_reflectance_value",
409
- * { min: 60 })`; bounds are inclusive, at least one is required, and a
410
- * non-numeric stored value is excluded rather than read as zero.
411
- */
412
- async bySpec(spec, options, signal) {
413
- return await this.#get("catalog_by_spec", {
414
- spec: Array.isArray(spec) ? spec : [spec],
415
- ...(options?.ranges?.length ? { specRange: options.ranges } : {}),
416
- limit: options?.limit,
417
- }, signal);
418
- }
419
- // ── Taxonomy + meta ──────────────────────────────────────────────────────────
420
- /**
421
- * THE QUESTION: "what kinds of thing exist, and what are their canonical keys?"
422
- * The source of every `categoryKey` / product-type id the other lenses accept
423
- * (`search({ category })`, `colours.profiles()`,
424
- * {@link MaterialGraphClient.applicableSpecs}). Discover, never hand-write.
425
- */
426
- async taxonomy(signal) {
427
- return await this.#get("get_category_taxonomy", undefined, signal);
428
- }
384
+ // Hard-specification search (formerly `bySpec()`, GET
385
+ // `/api/v1/search/specifications`) moved to {@link MaterialGraphClient.listVariants}
386
+ // (MG-2222): each `attribute:value` criterion is one `filters` condition —
387
+ // `{field: attributeId, op: "in", values: [...]}` in `filters.terms` for a
388
+ // term, `{field: attributeId, op: "gte"/"lte"/etc, value}` in
389
+ // `filters.numbers` for a number — and every condition across the
390
+ // kind-grouped `filters` object must match. Use `codes.advisory()` when
391
+ // the real question is what a space demands, and {@link specificationFacets}
392
+ // first if you do not know the canonical tokens.
393
+ // ── Coverage + meta ──────────────────────────────────────────────────────────
429
394
  /**
430
395
  * Exact current primary-type status counts by retained source category path.
431
396
  * `comparisonThreshold` is a reporting cutoff only: it never promotes a
@@ -513,13 +478,12 @@ export class MaterialGraphClient extends CatalogueClient {
513
478
  async extractPalette(input, signal) {
514
479
  return await this.#post("extract_image_palette", input, signal);
515
480
  }
516
- // ── Catalog (facets / applicable specs / glossary) ───────────────────────────
481
+ // ── Catalog (facets / glossary) ─────────────────────────────────────────────
517
482
  /**
518
483
  * THE QUESTION: "what can I even filter on, and what values does the corpus
519
484
  * actually hold?" The start of every spec-driven workflow. Instead use
520
- * {@link MaterialGraphClient.attributeValues} for ONE attribute's distribution,
521
- * {@link MaterialGraphClient.applicableSpecs} for what a product type COULD
522
- * carry, {@link MaterialGraphClient.schema} for the definition.
485
+ * {@link MaterialGraphClient.listAttributes} with `categoryIds` for what a
486
+ * product type COULD carry, {@link MaterialGraphClient.schema} for the definition.
523
487
  *
524
488
  * Discover the live specification vocabulary (GET
525
489
  * `/api/v1/catalog/specifications/facets`) — the facetable spec attributes that
@@ -528,58 +492,19 @@ export class MaterialGraphClient extends CatalogueClient {
528
492
  * exists: LEED, GREENGUARD, ASTM E84 Class A, FloorScore, EPDs all surface here
529
493
  * with counts. Includes the virtual `certification_programme` facet (its values
530
494
  * are programme names). Learn an `attribute:value` here, then reuse it as a
531
- * {@link bySpec} criterion or a {@link search} `filters` entry. NOTE: continuous
495
+ * {@link MaterialGraphClient.listVariants} filter leaf or a {@link search}
496
+ * `filters` entry. NOTE: continuous
532
497
  * numeric specs (e.g. `slip_resistance_dcof`, a 0–1 DCOF range) are NOT
533
498
  * token-facetable and do not appear here — read those per-attribute.
534
499
  */
535
500
  async specificationFacets(signal) {
536
501
  return await this.#get("list_spec_facets", undefined, signal);
537
502
  }
538
- /**
539
- * List the live value distribution for ONE canonical attribute (GET
540
- * `/api/v1/catalog/attributes/{attributeId}/values`), ordered by cross-brand
541
- * reach with variant + brand counts. The drill-in companion to
542
- * {@link specificationFacets} — e.g. every certification programme on record,
543
- * or every fire classification and how many variants carry each.
544
- */
545
- async attributeValues(attributeId, signal) {
546
- return await this.#get("list_attribute_values", undefined, signal, {
547
- attributeId,
548
- });
549
- }
550
- /**
551
- * THE QUESTION: "what specs COULD a material of this type carry?" — the
552
- * template, not the data. Use it to know which absences are meaningful; instead
553
- * use {@link MaterialGraphClient.specificationFacets} for what the corpus holds
554
- * and {@link MaterialGraphClient.materialBlocks} for one material.
555
- *
556
- * @param key A canonical taxonomy key from {@link MaterialGraphClient.taxonomy}.
557
- */
558
- async applicableSpecs(key, signal) {
559
- return await this.#get("get_applicable_specs", undefined, signal, { key });
560
- }
561
- /**
562
- * THE QUESTION: "what does 'DCOF' MEAN?" — one term explained, for a human.
563
- * Explanation, not retrieval: instead use
564
- * {@link MaterialGraphClient.searchVocabulary} to FIND a token and
565
- * {@link MaterialGraphClient.attributeValues} for the values it takes.
566
- */
567
- async glossary(term, signal) {
568
- return await this.#get("explain_material_term", undefined, signal, { term });
569
- }
570
503
  // ── Brands ───────────────────────────────────────────────────────────────────
571
504
  /** "Whose catalogs are in here, and how big is each?" — {@link listBrands}. */
572
505
  async brands(options = {}, signal) {
573
506
  return await listBrands(this.#transport, options, signal);
574
507
  }
575
- /** "Who is this brand, and what do they claim?" — {@link brandProfile}. */
576
- async brand(brandSlug, options, signal) {
577
- return await brandProfile(this.#transport, brandSlug, options, signal);
578
- }
579
- /** "This maker's whole range, the way a fan deck reads" — {@link brandColourDeck}. */
580
- async brandColourDeck(brandSlug, signal) {
581
- return await brandColourDeck(this.#transport, brandSlug, signal);
582
- }
583
508
  // ── Runs (the hosted agentic surface) ────────────────────────────────────────
584
509
  /**
585
510
  * Upload one atomic source-material batch (POST `/api/v1/source-material`,
@@ -601,22 +526,11 @@ export class MaterialGraphClient extends CatalogueClient {
601
526
  async sourceMaterialBatch(batchId, signal) {
602
527
  return await getSourceMaterialBatchCall(this.#transport, batchId, signal);
603
528
  }
604
- /**
605
- * THE QUESTION: "what can the hosted agentic surface be asked to DO?" Start
606
- * here before {@link MaterialGraphClient.createRun}: each capability carries its
607
- * live input/output schemas, scopes and side-effect class, so nothing about a
608
- * Run need be hardcoded.
609
- */
610
- async capabilities(signal) {
611
- return await capabilitiesRun(this.#transport, signal);
612
- }
613
529
  /**
614
530
  * THE QUESTION: "do a whole job for me" — the ASYNC lens. Everything else here
615
531
  * answers in one round trip; a Run may suspend for human input and finish
616
532
  * minutes later. Follow with {@link MaterialGraphClient.streamRunEvents} (live)
617
- * or {@link MaterialGraphClient.run} (poll); resume via
618
- * {@link MaterialGraphClient.resumeRun}; pick the capability from
619
- * {@link MaterialGraphClient.capabilities}. Pass `idempotencyKey` so a retry
533
+ * or {@link MaterialGraphClient.run} (poll). Pass `idempotencyKey` so a retry
620
534
  * replays rather than duplicates.
621
535
  */
622
536
  async createRun(body, signal) {
@@ -625,54 +539,15 @@ export class MaterialGraphClient extends CatalogueClient {
625
539
  /**
626
540
  * THE QUESTION: "where has this Run got to?" — the caller-owned, redacted
627
541
  * ledger record. Polling; prefer {@link MaterialGraphClient.streamRunEvents}
628
- * when a human is waiting. `suspended` is not a failure — it means the Run is
629
- * asking you something (see {@link MaterialGraphClient.resumeRun}).
542
+ * when a human is waiting.
630
543
  */
631
544
  async run(runId, signal) {
632
545
  return await getRun(this.#transport, runId, signal);
633
546
  }
634
- /**
635
- * THE QUESTION: "which Runs are mine?" — your own Runs, newest first, paged
636
- * (GET `/api/v1/runs`). This is how a client that lost a Run id finds its work
637
- * again: `{ status: "suspended" }` returns the Runs still waiting on an
638
- * answer, whose ids you can then feed to {@link MaterialGraphClient.resumeRun}.
639
- *
640
- * Only your own Runs are ever returned. Page by following `nextCursor` until
641
- * it is ABSENT — a full page is not the signal to stop.
642
- */
643
- async listRuns(options) {
644
- return await listRuns(this.#transport, options);
645
- }
646
547
  /** Follow a verdict cell's citation handle to its canonical claim source. */
647
548
  async citation(citationId, signal) {
648
549
  return await this.#get("get_verdict_citation", undefined, signal, { citationId });
649
550
  }
650
- /**
651
- * Resume a suspended Run (POST `/api/v1/runs/{runId}/resume`).
652
- *
653
- * The third argument accepts either an `AbortSignal` (the published form) or
654
- * {@link ResumeRunOptions}, whose `stepId` guards against resuming a step the
655
- * run has already moved past — the API answers 409 on a mismatch (MG-608).
656
- */
657
- async resumeRun(runId, resumeData, signalOrOptions) {
658
- return await resumeRun(this.#transport, runId, resumeData, signalOrOptions);
659
- }
660
- /**
661
- * THE QUESTION: "stop this Run." Requests DURABLE cancellation — the Run
662
- * acknowledges and unwinds; the call returning does not mean it has stopped.
663
- * Poll {@link MaterialGraphClient.run} for the terminal state.
664
- */
665
- async cancelRun(runId, signal) {
666
- return await cancelRun(this.#transport, runId, signal);
667
- }
668
- /** Option B from a composed Run, along one axis. See {@link varySchemeCall}. */
669
- async varyScheme(body, signal) {
670
- return await varySchemeCall(this.#transport, body, signal);
671
- }
672
- /** Ranked candidate pools for up to 24 slots. See {@link slotPoolsCall}. */
673
- async slotPools(body, signal) {
674
- return await slotPoolsCall(this.#transport, body, signal);
675
- }
676
551
  /**
677
552
  * Stream ordered Run events as an async iterator (GET
678
553
  * `/api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or the
package/codes-types.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * model code as adopted, never a legal determination.
15
15
  */
16
16
  import type { VariantId } from "./identifiers.js";
17
+ import type { FilterGroups } from "./list-variants-types.js";
17
18
  import type { OpenEnum } from "./types.js";
18
19
  /**
19
20
  * A code context: the space the registry is queried for.
@@ -292,6 +293,14 @@ export type CodeRequirementListing = {
292
293
  /** One readable line, e.g. `"Class A or better · ASTM E84 / UL 723"`. */
293
294
  label: string;
294
295
  };
296
+ /** Why one requirement the context imposes could not become a `list_variants` filter predicate. */
297
+ export type CodeSpecFilterUnexpressible = {
298
+ requirementId: string;
299
+ dimension: OpenEnum<"fire" | "floor" | "slip" | "decorative">;
300
+ reason: OpenEnum<"no_matchable_tokens" | "unsupported_scale" | "no_floor">;
301
+ /** One readable line naming why, e.g. `"Scale ... has no ordinal definition..."`. */
302
+ detail: string;
303
+ };
295
304
  /**
296
305
  * The `GET /api/v1/codes/requirements` envelope. An empty `requirements` array
297
306
  * means the registry holds nothing for this context — it is not a pass.
@@ -300,5 +309,17 @@ export type CodeRequirementsResponse = {
300
309
  jurisdiction: CodeJurisdictionSummary;
301
310
  context: ResolvedCodeContext;
302
311
  requirements: CodeRequirementListing[];
312
+ /**
313
+ * The same requirements as a ready-to-use `list_variants` `filters` group
314
+ * (MG-2222, `{terms, numbers, presence}`) — pass it straight through as
315
+ * `listVariants({ filters })`.
316
+ * `null` when nothing here is filterable; read `specFilterUnexpressible`
317
+ * for why, not an empty `requirements` list.
318
+ */
319
+ specFilter: FilterGroups | null;
320
+ /** The dimensions `specFilter` actually narrows on — a subset of `requirements`' dimensions. */
321
+ specFilterApplied: OpenEnum<"fire" | "floor" | "slip" | "decorative">[];
322
+ /** Requirements that could not be turned into a filter predicate, and why. */
323
+ specFilterUnexpressible: CodeSpecFilterUnexpressible[];
303
324
  caveat: string;
304
325
  };