@instruments/materialgraph 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -28,18 +28,69 @@ function operation(operationId) {
28
28
  }
29
29
  return match;
30
30
  }
31
+ /**
32
+ * The MaterialGraph client — every lens on the graph, on one object.
33
+ *
34
+ * PICK YOUR LENS BY THE QUESTION, not by the noun. Most wrong answers from this
35
+ * API come from routing a question to a lens that cannot see it:
36
+ *
37
+ * | The question | The lens |
38
+ * | --- | --- |
39
+ * | "find me something like this / matching these words" | {@link MaterialGraphClient.search} |
40
+ * | "what's IN this photo?" | {@link MaterialGraphClient.match} / {@link MaterialGraphClient.matchStream} |
41
+ * | "which materials, from anyone, meet this hard spec?" | {@link MaterialGraphClient.bySpec} |
42
+ * | "what's near this colour / this palette / this gradient?" | {@link MaterialGraphClient.byColour}, `colours.palette()`, `colours.gradient()` |
43
+ * | "what goes with THIS material?" | {@link MaterialGraphClient.pairings}, {@link MaterialGraphClient.siblings}, {@link MaterialGraphClient.coordinate} |
44
+ * | "how much is this specified?" | {@link MaterialGraphClient.demand}, {@link MaterialGraphClient.demandBatch} |
45
+ * | "is this allowed in an A-2 corridor?" | `codes.*` |
46
+ * | "what can I even ask for?" | {@link MaterialGraphClient.specificationFacets}, {@link MaterialGraphClient.attributeValues}, {@link MaterialGraphClient.taxonomy}, {@link MaterialGraphClient.colourFacets}, {@link MaterialGraphClient.glossary} |
47
+ * | "do the whole job for me, async" | {@link MaterialGraphClient.createRun} + `mg:compose` |
48
+ *
49
+ * Two structural rules that fall out of that table:
50
+ * - **Colour is a separate lens.** {@link MaterialGraphClient.search} matches
51
+ * WORDS. A colour word in `text` finds materials whose text mentions it, not
52
+ * materials that look it. Route visual intent to the colour lenses.
53
+ * - **Relationships hang off a material, not off a query.** You cannot ask "what
54
+ * pairs with beige oak"; you resolve a material first, then ask what pairs
55
+ * with THAT `variantId`.
56
+ *
57
+ * And three reading rules that apply to every lens: ids are opaque (see
58
+ * `./identifiers.ts`), absent is not false, and a page is not a corpus.
59
+ */
31
60
  export class MaterialGraphClient {
32
61
  baseUrl;
62
+ /**
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.
67
+ *
68
+ * Instead use {@link MaterialGraphClient.byColour} for materials NEAR a colour,
69
+ * {@link MaterialGraphClient.search} when there is anything to rank against,
70
+ * and `colours.profiles()` for a category's palette rather than its members.
71
+ */
33
72
  browse;
34
73
  /**
35
- * The advisory building-code surface: `codes.context()` to discover the
36
- * vocabulary, `codes.requirements()` to state a space, `codes.advisory()` to
37
- * badge candidates against it.
74
+ * THE QUESTION: "is this material allowed HERE?" — a jurisdiction + occupancy +
75
+ * finish location, not a property of the material alone. `codes.context()`
76
+ * discovers the vocabulary, `codes.requirements()` states a space,
77
+ * `codes.advisory()` badges candidates.
78
+ *
79
+ * Instead use {@link MaterialGraphClient.bySpec} to find materials by a
80
+ * classification you already know you need; codes is what tells you WHICH one
81
+ * the space demands. ADVISORY ONLY: model code as adopted, never a
82
+ * determination for an authority having jurisdiction.
38
83
  */
39
84
  codes;
40
85
  /**
41
- * The colour AGGREGATE surface: `colours.profiles()` for a category palette,
42
- * `colours.stats()` for corpus colour coverage.
86
+ * THE QUESTION: "what colours exist here?" (`profiles`, `stats`) and "what
87
+ * matches this whole SET of colours?" (`palette`, `gradient`).
88
+ *
89
+ * Instead use {@link MaterialGraphClient.byColour} for ONE colour,
90
+ * {@link MaterialGraphClient.browse} to walk by a colour scalar, and
91
+ * {@link MaterialGraphClient.colourFacets} for the colour VOCABULARY rather
92
+ * than values. Colour throughout is derived from product imagery — visual
93
+ * likeness, never a declared colour specification.
43
94
  */
44
95
  colours;
45
96
  #transport;
@@ -73,6 +124,16 @@ export class MaterialGraphClient {
73
124
  return this.#transport.requestJson(operation(operationId), { body, signal });
74
125
  }
75
126
  /**
127
+ * THE QUESTION: "find me materials like this" — a RANKED answer over words, a
128
+ * seed image, colours and a mood, optionally gated by hard specs.
129
+ *
130
+ * Instead use {@link MaterialGraphClient.bySpec} when the spec IS the question
131
+ * (ranking would be noise), {@link MaterialGraphClient.byColour} /
132
+ * `colours.palette()` for visual colour intent (`text` matches WORDS — "sage
133
+ * green" finds materials that SAY sage, not ones that look it),
134
+ * {@link MaterialGraphClient.match} for a photo, {@link MaterialGraphClient.pairings}
135
+ * for what goes WITH something, {@link MaterialGraphClient.suggest} for typeahead.
136
+ *
76
137
  * The full search primitive (POST `/api/v1/search`). Layer a hard
77
138
  * specification gate onto a ranked (colour / visual / lexical / seed) search
78
139
  * via `input.filters` — each entry is a typed {@link SearchFilter} criterion:
@@ -86,9 +147,28 @@ export class MaterialGraphClient {
86
147
  search(input, signal) {
87
148
  return this.#post("search_materials_post", input, signal);
88
149
  }
150
+ /**
151
+ * The same question as {@link MaterialGraphClient.search}; the difference is
152
+ * transport — a cacheable, link-shareable GET for queries that must survive in
153
+ * a URL. `filters`, `preferences`, `verdicts`, image bytes and `weights` have
154
+ * no query-string form, so anything using those must go through `search`.
155
+ */
89
156
  searchGet(params, signal) {
90
157
  return this.#get("search_materials_get", { ...params }, signal);
91
158
  }
159
+ /**
160
+ * THE QUESTION: "what materials are in this PHOTO?" — Material Match classifies
161
+ * the image, decomposes it into surfaces, and answers each one.
162
+ *
163
+ * Instead use {@link MaterialGraphClient.matchStream} whenever a human is
164
+ * watching, {@link MaterialGraphClient.search} with `image` for ONE ranked list
165
+ * rather than a per-surface decomposition, and
166
+ * {@link MaterialGraphClient.extractPalette} for the image's colours only.
167
+ *
168
+ * Exactly one of `imageBase64`, `imageUrl` or `vector`. Read `qualityFloor` and
169
+ * `droppedBelowFloor` before reporting an empty result: "nothing was good
170
+ * enough" and "nothing was found" are different answers.
171
+ */
92
172
  match(input, signal) {
93
173
  return this.#post("match_material", input, signal);
94
174
  }
@@ -118,6 +198,17 @@ export class MaterialGraphClient {
118
198
  matchStream(input, signal) {
119
199
  return matchStreamCall(this.#transport, input, signal);
120
200
  }
201
+ /**
202
+ * THE QUESTION: "what materials are near THIS colour?" — one colour (or a small
203
+ * palette), ranked by OKLab closeness to colour extracted from product imagery.
204
+ *
205
+ * Instead use `colours.palette()` for N colours at stated proportions (a
206
+ * different ranking, not a wrapper), `colours.gradient()` for a transition,
207
+ * {@link MaterialGraphClient.resolveColour} first if you hold a NAME,
208
+ * {@link MaterialGraphClient.browse} to walk by lightness/vividness, and
209
+ * {@link MaterialGraphClient.search} when colour is one signal among many.
210
+ * Report matches as visual likeness, never a stated colour specification.
211
+ */
121
212
  byColour(params, signal) {
122
213
  return this.#get("search_by_colour", {
123
214
  hex: params.hex,
@@ -130,33 +221,82 @@ export class MaterialGraphClient {
130
221
  }, signal);
131
222
  }
132
223
  // ── Materials ──────────────────────────────────────────────────────────────
133
- /** Get a single material's detail record (GET `/api/v1/materials/{variantId}`). */
224
+ /**
225
+ * THE QUESTION: "tell me everything about THIS material" (GET
226
+ * `/api/v1/materials/{variantId}`).
227
+ *
228
+ * The hinge of the API: every relationship lens
229
+ * ({@link MaterialGraphClient.siblings}, {@link MaterialGraphClient.pairings},
230
+ * {@link MaterialGraphClient.demand}, `codes.advisory()`) hangs off a
231
+ * `variantId`. Instead use {@link MaterialGraphClient.materials} for many ids,
232
+ * {@link MaterialGraphClient.materialBlocks} to tell "no value" from "not
233
+ * checked", {@link MaterialGraphClient.search} when you have no id yet.
234
+ *
235
+ * @param variantId Opaque — pass back exactly what you were given.
236
+ */
134
237
  material(variantId, options, signal) {
135
238
  return this.#get("get_material", options?.includeSiblings ? { include: "siblings" } : undefined, signal, { variantId });
136
239
  }
137
- /** The other variants of the same product (GET `/api/v1/materials/{variantId}/siblings`). */
240
+ /**
241
+ * THE QUESTION: "what other colourways and finishes does this product come in?"
242
+ * — the SAME product, different variants.
243
+ *
244
+ * Instead use {@link MaterialGraphClient.pairings} for a DIFFERENT product that
245
+ * goes with this one, and `search({ similarTo })` for one that LOOKS like it.
246
+ */
138
247
  siblings(variantId, signal) {
139
248
  return this.#get("get_material_siblings", undefined, signal, { variantId });
140
249
  }
141
- /** Canonical block completeness for a material (GET `/api/v1/materials/{variantId}/blocks`). */
250
+ /**
251
+ * THE QUESTION: "what do we actually KNOW about this material, and what have we
252
+ * simply not checked?" The only read that distinguishes a determined absence
253
+ * from an unasked question — every field carries a `status`, so this is the one
254
+ * surface entitled to back a negative ("no fire rating").
255
+ * {@link MaterialGraphClient.material} cannot: there an absence is merely unknown.
256
+ */
142
257
  materialBlocks(variantId, signal) {
143
258
  return this.#get("list_material_blocks", undefined, signal, {
144
259
  variantId,
145
260
  });
146
261
  }
147
- /** Safe images and downloads for a material (GET `/api/v1/materials/{variantId}/assets`). */
262
+ /**
263
+ * THE QUESTION: "what images and documents may I SHOW for this material?" — the
264
+ * redistribution-safe set (GET `/api/v1/materials/{variantId}/assets`), not
265
+ * every file on record.
266
+ */
148
267
  materialAssets(variantId, signal) {
149
268
  return this.#get("list_material_assets", undefined, signal, {
150
269
  variantId,
151
270
  });
152
271
  }
153
- /** Read one static governed schema resource. */
272
+ /**
273
+ * THE QUESTION: "what does the schema DEFINE?" — the governed static registry.
274
+ *
275
+ * Definitions, not data: use {@link MaterialGraphClient.specificationFacets} for
276
+ * what the corpus actually holds and {@link MaterialGraphClient.glossary} to
277
+ * explain one term.
278
+ */
154
279
  schema(resource, signal) {
155
280
  return this.#get(SCHEMA_RESOURCE_OPERATION_IDS[resource], undefined, signal);
156
281
  }
157
- /** The "what pairs with this material" rail (GET `/api/v1/materials/{variantId}/pairings`). */
282
+ /**
283
+ * THE QUESTION: "what goes WITH this material?" — the companionship rail, hung
284
+ * off one resolved material (GET `/api/v1/materials/{variantId}/pairings`).
285
+ *
286
+ * Instead use {@link MaterialGraphClient.coordinate} for a whole palette of
287
+ * picks, {@link MaterialGraphClient.siblings} for the same product in another
288
+ * colour, and `search({ similarTo })` for a LIKE-THIS substitute — similar and
289
+ * complementary are opposite questions.
290
+ *
291
+ * READ `item.source` BEFORE PHRASING: the rail mixes three tiers of evidence —
292
+ * `cospec` (real orders), `considered` (board activity), `colour` (palette
293
+ * look-alike). Only `cospec` supports "specified together". `cospecCount` tells
294
+ * you how much of the rail is real order evidence.
295
+ */
158
296
  pairings(variantId, options, signal) {
159
- return this.#get("get_material_pairings", { categories: options?.categories, limit: options?.limit }, signal, { variantId });
297
+ return this.#get("get_material_pairings", { ...options }, signal, {
298
+ variantId,
299
+ });
160
300
  }
161
301
  materials(ids, options, signal) {
162
302
  if (ids.length > MAX_BATCH_IDS) {
@@ -175,11 +315,20 @@ export class MaterialGraphClient {
175
315
  }, signal);
176
316
  }
177
317
  // ── Colour helpers ───────────────────────────────────────────────────────────
178
- /** The three canonical colour vocabularies with live counts (GET `/api/v1/colours/facets`). */
318
+ /**
319
+ * THE QUESTION: "what colour WORDS may I use?" — family, mood and undertone
320
+ * with live counts. Vocabulary, not values: instead use `colours.profiles()`
321
+ * for a category's palette, {@link MaterialGraphClient.byColour} to search by a
322
+ * colour, {@link MaterialGraphClient.resolveColour} to turn a NAME into a hex.
323
+ */
179
324
  colourFacets(signal) {
180
325
  return this.#get("get_colour_facets", undefined, signal);
181
326
  }
182
- /** Resolve a colour name/phrase to a hex (GET `/api/v1/search/colours/resolve`). */
327
+ /**
328
+ * THE QUESTION: "what hex is 'Hague Blue'?" — the step before
329
+ * {@link MaterialGraphClient.byColour} when you hold a name rather than a hex.
330
+ * A `null` match means unrecognised, not "no such colour".
331
+ */
183
332
  resolveColour(name, signal) {
184
333
  return this.#get("resolve_colour_name", { name }, signal);
185
334
  }
@@ -188,6 +337,11 @@ export class MaterialGraphClient {
188
337
  return this.#get("suggest_colour_names", { q, limit: options?.limit }, signal);
189
338
  }
190
339
  /**
340
+ * THE QUESTION: "what is the user starting to type?" It ranks NAMES, not
341
+ * materials — instead use {@link MaterialGraphClient.search} once they commit,
342
+ * {@link MaterialGraphClient.suggestColour} for colour names, and
343
+ * {@link MaterialGraphClient.searchVocabulary} for attribute/value tokens.
344
+ *
191
345
  * Entity typeahead across products, brands and categories
192
346
  * (GET `/api/v1/search/suggest`). Built for per-keystroke use: one indexed
193
347
  * lookup, no embedding call and no model. Every suggestion carries the
@@ -202,19 +356,53 @@ export class MaterialGraphClient {
202
356
  return this.#get("search_suggest", suggestQuery(q, options), signal);
203
357
  }
204
358
  // ── Catalog ──────────────────────────────────────────────────────────────────
205
- /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
359
+ /**
360
+ * THE QUESTION: "the user said 'fire rating' — which canonical attribute is
361
+ * that?" Free text in, canonical `attributeId`s out; pair with
362
+ * {@link MaterialGraphClient.resolveAttributeValue} for the VALUE, then spend
363
+ * the pair on {@link MaterialGraphClient.bySpec} or `search({ filters })`.
364
+ * Instead use {@link MaterialGraphClient.specificationFacets} to browse rather
365
+ * than resolve.
366
+ */
206
367
  resolveAttribute(q, options, signal) {
207
368
  return this.#get("catalog_resolve_attribute", { q, limit: options?.limit, facetableOnly: options?.facetableOnly }, signal);
208
369
  }
209
- /** Resolve a value phrase against one canonical attribute's dictionary. */
370
+ /**
371
+ * THE QUESTION: "the user said 'class A' — what canonical term is that, under
372
+ * THIS attribute?" The value half of the pair
373
+ * {@link MaterialGraphClient.resolveAttribute} starts. Read
374
+ * `resolution.recommendedAction`, not just `best`: `ambiguous` means ask.
375
+ *
376
+ * @param attributeId Canonical vocabulary, not an opaque id — discover it, do
377
+ * not invent it.
378
+ */
210
379
  resolveAttributeValue(attributeId, query, options, signal) {
211
380
  return this.#get("catalog_resolve_value", { q: query, limit: options?.limit }, signal, { attributeId });
212
381
  }
213
- /** Search canonical attributes and value-dictionary terms together. */
382
+ /**
383
+ * THE QUESTION: "is 'FloorScore' an attribute or a value, and what is it called
384
+ * here?" The catch-all when you do not know which half of an `attribute:value`
385
+ * pair a phrase belongs to. Instead use
386
+ * {@link MaterialGraphClient.resolveAttribute} /
387
+ * {@link MaterialGraphClient.resolveAttributeValue} once you do, and
388
+ * {@link MaterialGraphClient.glossary} to EXPLAIN a term rather than find it.
389
+ */
214
390
  searchVocabulary(query, options, signal) {
215
391
  return this.#get("catalog_vocabulary_search", { q: query, limit: options?.limit, kind: options?.kind }, signal);
216
392
  }
217
393
  /**
394
+ * THE QUESTION: "which materials, from ANYONE, are ASTM E84 Class A AND
395
+ * GREENGUARD-certified?" — hard specifications, no ranking, every brand at once
396
+ * whatever each called the value in source.
397
+ *
398
+ * Instead use `search({ filters })` when the spec merely GATES a ranked
399
+ * question, `codes.advisory()` when the real question is what a space demands,
400
+ * and {@link MaterialGraphClient.specificationFacets} first if you do not know
401
+ * the canonical tokens — guessing one returns an honest, empty answer.
402
+ *
403
+ * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
404
+ * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
405
+ *
218
406
  * Find materials by canonical specification across every brand — the
219
407
  * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
220
408
  * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
@@ -229,16 +417,31 @@ export class MaterialGraphClient {
229
417
  return this.#get("catalog_by_spec", { spec: Array.isArray(spec) ? spec : [spec], limit: options?.limit }, signal);
230
418
  }
231
419
  // ── Taxonomy + meta ──────────────────────────────────────────────────────────
232
- /** The product-category tree, flattened (GET `/api/v1/taxonomy/categories`). */
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
+ */
233
426
  taxonomy(signal) {
234
427
  return this.#get("get_category_taxonomy", undefined, signal);
235
428
  }
236
- /** Echo the authenticated principal and scopes (GET `/api/v1/whoami`). */
429
+ /**
430
+ * THE QUESTION: "what is this key allowed to do?" Check `scopes` before
431
+ * offering a capability: the write surfaces need `mg:ingest` / `mg:compose`,
432
+ * and `mg:admin` unlocks extra evidence fields.
433
+ */
237
434
  whoami(signal) {
238
435
  return this.#get("whoami", undefined, signal);
239
436
  }
240
437
  // ── Pairings / demand ────────────────────────────────────────────────────────
241
438
  /**
439
+ * THE QUESTION: "here is my whole scheme so far — what completes it?" The
440
+ * multi-seed form of {@link MaterialGraphClient.pairings}: lift is summed across
441
+ * seeds, so a companion suiting one pick ranks below one suiting several.
442
+ * Instead use `pairings` for a single material and `colours.palette()` when the
443
+ * scheme is COLOURS rather than materials. Check `unresolved` before presenting.
444
+ *
242
445
  * Coordinate a palette of already-chosen materials (POST
243
446
  * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
244
447
  * colour swatches and an annotate-only `colourFit` (never reorders in v1).
@@ -247,13 +450,25 @@ export class MaterialGraphClient {
247
450
  return this.#post("coordinate_materials", input, signal);
248
451
  }
249
452
  /**
250
- * How much a material is specified, plus what's specified alongside it (GET
251
- * `/api/v1/demand`). Provide EXACTLY ONE of `variantId` or `material`.
453
+ * THE QUESTION: "how much is this material actually specified, and what gets
454
+ * specified alongside it?" — real order behaviour, trailing 36 months.
455
+ *
456
+ * Instead use {@link MaterialGraphClient.demandBatch} for many ids (this one
457
+ * also resolves free text, which the batch deliberately does not) and
458
+ * {@link MaterialGraphClient.pairings} for a presentable rail. A null `demand`
459
+ * means UNREAD, never zero. Provide EXACTLY ONE of `variantId` or `material`.
252
460
  */
253
461
  demand(params, signal) {
254
462
  return this.#get("get_material_demand", { variantId: params.variantId, material: params.material }, signal);
255
463
  }
256
464
  /**
465
+ * THE QUESTION: "for each of these picks I already hold, how much momentum does
466
+ * it have?" Enrichment, never discovery or ranking. Instead use
467
+ * {@link MaterialGraphClient.demand} for one material named in free text and
468
+ * {@link MaterialGraphClient.search} to find candidates at all. Only
469
+ * `state: "available"` carries numbers; the other three mean "no number", never
470
+ * "no demand".
471
+ *
257
472
  * Enrich a bounded set of ALREADY-CHOSEN candidate ids with their real
258
473
  * specification demand (POST `/api/v1/demand/batch`, 1–48 ids). The bounded
259
474
  * batch complement to {@link demand}: no fuzzy resolution, ranking, or
@@ -278,20 +493,36 @@ export class MaterialGraphClient {
278
493
  }
279
494
  // ── Codes (advisory building-code compliance) ────────────────────────────────
280
495
  /**
496
+ * THE QUESTION: "may I put these materials in an A-2 corridor in Texas?"
497
+ *
281
498
  * Badge materials against the advisory building-code registry (POST
282
499
  * `/api/v1/codes/advisory`). Identical to {@link CodesClient.advisory}, kept
283
- * flat because it shipped before the `codes` namespace existed.
500
+ * flat because it shipped before the `codes` namespace existed — prefer
501
+ * `mg.codes.advisory()` in new code, and start at `mg.codes.context()` to
502
+ * discover every accepted context value. `unverified` is not a pass.
284
503
  */
285
504
  codeAdvisory(input, signal) {
286
505
  return this.codes.advisory(input, signal);
287
506
  }
288
507
  // ── Colour helpers (extract) ─────────────────────────────────────────────────
289
- /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
508
+ /**
509
+ * THE QUESTION: "what colours are in this image?" — the palette only, no
510
+ * materials. The bridge from an inspiration image into the colour lens: extract
511
+ * here, then feed the swatches to `colours.palette()` or
512
+ * {@link MaterialGraphClient.byColour}. Instead use
513
+ * {@link MaterialGraphClient.match} for the MATERIALS in the photo.
514
+ */
290
515
  extractPalette(input, signal) {
291
516
  return this.#post("extract_image_palette", input, signal);
292
517
  }
293
518
  // ── Catalog (facets / applicable specs / glossary) ───────────────────────────
294
519
  /**
520
+ * THE QUESTION: "what can I even filter on, and what values does the corpus
521
+ * actually hold?" The start of every spec-driven workflow. Instead use
522
+ * {@link MaterialGraphClient.attributeValues} for ONE attribute's distribution,
523
+ * {@link MaterialGraphClient.applicableSpecs} for what a product type COULD
524
+ * carry, {@link MaterialGraphClient.schema} for the definition.
525
+ *
295
526
  * Discover the live specification vocabulary (GET
296
527
  * `/api/v1/catalog/specifications/facets`) — the facetable spec attributes that
297
528
  * have real product-graph data, each with its top canonical values and the
@@ -318,20 +549,43 @@ export class MaterialGraphClient {
318
549
  attributeId,
319
550
  });
320
551
  }
321
- /** The specs a product type COULD carry (GET `/api/v1/taxonomy/categories/{key}/specs`). */
552
+ /**
553
+ * THE QUESTION: "what specs COULD a material of this type carry?" — the
554
+ * template, not the data. Use it to know which absences are meaningful; instead
555
+ * use {@link MaterialGraphClient.specificationFacets} for what the corpus holds
556
+ * and {@link MaterialGraphClient.materialBlocks} for one material.
557
+ *
558
+ * @param key A canonical taxonomy key from {@link MaterialGraphClient.taxonomy}.
559
+ */
322
560
  applicableSpecs(key, signal) {
323
561
  return this.#get("get_applicable_specs", undefined, signal, { key });
324
562
  }
325
- /** Resolve a term to its meaning in the schema (GET `/api/v1/glossary/{term}`). */
563
+ /**
564
+ * THE QUESTION: "what does 'DCOF' MEAN?" — one term explained, for a human.
565
+ * Explanation, not retrieval: instead use
566
+ * {@link MaterialGraphClient.searchVocabulary} to FIND a token and
567
+ * {@link MaterialGraphClient.attributeValues} for the values it takes.
568
+ */
326
569
  glossary(term, signal) {
327
570
  return this.#get("explain_material_term", undefined, signal, { term });
328
571
  }
329
572
  // ── Brands ───────────────────────────────────────────────────────────────────
330
- /** List registered brands (GET `/api/v1/brands`). */
573
+ /**
574
+ * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
575
+ * one OFFSET-paginated surface: read `meta.total` for the collection size and
576
+ * `meta.hasMore` to keep walking. Instead use {@link MaterialGraphClient.brand}
577
+ * for one brand's profile and trade signals.
578
+ */
331
579
  brands(options = {}, signal) {
332
580
  return this.#get("list_brands", { q: options.q, limit: options.limit, offset: options.offset }, signal);
333
581
  }
334
- /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{brandSlug}`). */
582
+ /**
583
+ * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
584
+ * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
585
+ * key means no value is held — never infer `false`. Instead use
586
+ * {@link MaterialGraphClient.search} to find their materials; this describes the
587
+ * company, not the catalog.
588
+ */
335
589
  brand(brandSlug, options, signal) {
336
590
  return this.#get("get_brand_profile", { q: options?.q }, signal, {
337
591
  brandSlug,
@@ -358,15 +612,33 @@ export class MaterialGraphClient {
358
612
  sourceMaterialBatch(batchId, signal) {
359
613
  return getSourceMaterialBatchCall(this.#transport, batchId, signal);
360
614
  }
361
- /** List invocable agents, workflows, and tools (GET `/api/v1/capabilities`). */
615
+ /**
616
+ * THE QUESTION: "what can the hosted agentic surface be asked to DO?" Start
617
+ * here before {@link MaterialGraphClient.createRun}: each capability carries its
618
+ * live input/output schemas, scopes and side-effect class, so nothing about a
619
+ * Run need be hardcoded.
620
+ */
362
621
  capabilities(signal) {
363
622
  return capabilitiesRun(this.#transport, signal);
364
623
  }
365
- /** Start an asynchronous capability Run (POST `/api/v1/runs`). */
624
+ /**
625
+ * THE QUESTION: "do a whole job for me" — the ASYNC lens. Everything else here
626
+ * answers in one round trip; a Run may suspend for human input and finish
627
+ * minutes later. Follow with {@link MaterialGraphClient.streamRunEvents} (live)
628
+ * or {@link MaterialGraphClient.run} (poll); resume via
629
+ * {@link MaterialGraphClient.resumeRun}; pick the capability from
630
+ * {@link MaterialGraphClient.capabilities}. Pass `idempotencyKey` so a retry
631
+ * replays rather than duplicates.
632
+ */
366
633
  createRun(body, signal) {
367
634
  return createRun(this.#transport, body, signal);
368
635
  }
369
- /** Get a Run's current state (GET `/api/v1/runs/{runId}`). */
636
+ /**
637
+ * THE QUESTION: "where has this Run got to?" — the caller-owned, redacted
638
+ * ledger record. Polling; prefer {@link MaterialGraphClient.streamRunEvents}
639
+ * when a human is waiting. `suspended` is not a failure — it means the Run is
640
+ * asking you something (see {@link MaterialGraphClient.resumeRun}).
641
+ */
370
642
  run(runId, signal) {
371
643
  return getRun(this.#transport, runId, signal);
372
644
  }
@@ -390,7 +662,11 @@ export class MaterialGraphClient {
390
662
  resumeRun(runId, resumeData, signalOrOptions) {
391
663
  return resumeRun(this.#transport, runId, resumeData, signalOrOptions);
392
664
  }
393
- /** Request durable Run cancellation (POST `/api/v1/runs/{runId}/cancel`). */
665
+ /**
666
+ * THE QUESTION: "stop this Run." Requests DURABLE cancellation — the Run
667
+ * acknowledges and unwinds; the call returning does not mean it has stopped.
668
+ * Poll {@link MaterialGraphClient.run} for the terminal state.
669
+ */
394
670
  cancelRun(runId, signal) {
395
671
  return cancelRun(this.#transport, runId, signal);
396
672
  }
@@ -421,7 +697,11 @@ export class MaterialGraphClient {
421
697
  createProject(body = {}, signal) {
422
698
  return createProjectCall(this.#transport, body, signal);
423
699
  }
424
- /** Read a project's assembled state (GET `/api/v1/projects/{projectId}`). */
700
+ /**
701
+ * THE QUESTION: "what has happened on this project?" — the append-only event log
702
+ * FOLDED into a read model, plus the raw `events`. Read the folded collections;
703
+ * the log is there for audit.
704
+ */
425
705
  getProject(projectId, signal) {
426
706
  return getProjectCall(this.#transport, projectId, signal);
427
707
  }
@@ -432,7 +712,10 @@ export class MaterialGraphClient {
432
712
  appendProjectEvent(projectId, body, signal) {
433
713
  return appendProjectEventCall(this.#transport, projectId, body, signal);
434
714
  }
435
- /** List the caller's own projects, newest first (GET `/api/v1/projects`). */
715
+ /**
716
+ * THE QUESTION: "what Boxes do I have?" Scoped to the authenticated principal —
717
+ * another key's projects are invisible, not forbidden.
718
+ */
436
719
  listProjects(signal) {
437
720
  return listProjectsCall(this.#transport, signal);
438
721
  }
package/codes-client.d.ts CHANGED
@@ -12,6 +12,16 @@
12
12
  *
13
13
  * ADVISORY ONLY: every verdict and every envelope carries the AHJ caveat — this
14
14
  * is model code as adopted, never a legal determination.
15
+ *
16
+ * ROUTING. Codes answers "is this material allowed in THIS SPACE?" — a question
17
+ * about a jurisdiction, an occupancy group and a finish location, not about a
18
+ * material alone.
19
+ * - "which materials carry Class A?" → `mg.bySpec("fire_classifications:...")`.
20
+ * That is a property question; this namespace is what tells you the space
21
+ * demands Class A in the first place.
22
+ * - "what does this one material hold?" → `mg.materialBlocks(variantId)`.
23
+ * - "find me something like this, but compliant" → `mg.search({ filters })`
24
+ * once `requirements()` has told you which filter to apply.
15
25
  */
16
26
  import type { CodeAdvisoryInput, CodeAdvisoryResponse, CodeContextRequest, CodeContextResponse, CodeRequirementsResponse } from "./codes-types.js";
17
27
  import type { QueryParams } from "./http-transport.js";
@@ -25,6 +35,13 @@ export declare class CodesClient {
25
35
  #private;
26
36
  constructor(transport: CodesTransport);
27
37
  /**
38
+ * THE QUESTION: "what may I even say about a space?" — the vocabulary for
39
+ * stating a code context, and the first call of the three.
40
+ *
41
+ * Everything {@link CodesClient.requirements} and {@link CodesClient.advisory}
42
+ * accept is listed here, so nothing need be hardcoded and a 400 for an unknown
43
+ * jurisdiction / occupancy / space is always avoidable.
44
+ *
28
45
  * The vocabulary for stating a code context (GET `/api/v1/codes/context`):
29
46
  * seeded jurisdictions and their coverage tier, the occupancy groups the
30
47
  * registry keys on, finish locations, dimensions, classification scales with
@@ -43,6 +60,12 @@ export declare class CodesClient {
43
60
  */
44
61
  context(signal?: AbortSignal): Promise<CodeContextResponse>;
45
62
  /**
63
+ * THE QUESTION: "what does this space DEMAND?" — before any candidate exists.
64
+ *
65
+ * Run it before shortlisting, not after: it tells you which specification to
66
+ * filter on, which you then spend on `mg.bySpec()` or `mg.search({ filters })`.
67
+ * Use {@link CodesClient.advisory} once you hold candidates to badge.
68
+ *
46
69
  * What a space demands, before any candidate exists (GET
47
70
  * `/api/v1/codes/requirements`). State the space by explicit axes, or by a
48
71
  * `roomType` the axes are correlated from.
@@ -61,6 +84,19 @@ export declare class CodesClient {
61
84
  */
62
85
  requirements(input: CodeContextRequest, signal?: AbortSignal): Promise<CodeRequirementsResponse>;
63
86
  /**
87
+ * THE QUESTION: "do THESE candidates satisfy what this space demands?" — the
88
+ * third call, once you hold materials.
89
+ *
90
+ * Context is CONDITIONAL: supply `occupancyGroup` + `spaceType`, or a
91
+ * `roomType` for them to correlate from, or a mix — an explicit axis always
92
+ * wins, and an axis that resolves to nothing is a 400 naming it. The full rule,
93
+ * with the validator's own messages, is on {@link CodeContextRequest}.
94
+ *
95
+ * Read `summary.status` honestly: `unverified` means no comparable rating is on
96
+ * file. It is not a pass, and it is common. Use {@link CodesClient.requirements}
97
+ * when you have no candidates yet, and `mg.materialBlocks()` to see whether a
98
+ * material's fire/slip facts are missing or merely unverified.
99
+ *
64
100
  * Badge materials against the advisory building-code registry for one context
65
101
  * (POST `/api/v1/codes/advisory`, ≤ 100 ids). Each applicable requirement is
66
102
  * badged `meets` / `shortfall` / `unknown` from the material's accepted fire
package/codes-client.js CHANGED
@@ -4,6 +4,13 @@ export class CodesClient {
4
4
  this.#transport = transport;
5
5
  }
6
6
  /**
7
+ * THE QUESTION: "what may I even say about a space?" — the vocabulary for
8
+ * stating a code context, and the first call of the three.
9
+ *
10
+ * Everything {@link CodesClient.requirements} and {@link CodesClient.advisory}
11
+ * accept is listed here, so nothing need be hardcoded and a 400 for an unknown
12
+ * jurisdiction / occupancy / space is always avoidable.
13
+ *
7
14
  * The vocabulary for stating a code context (GET `/api/v1/codes/context`):
8
15
  * seeded jurisdictions and their coverage tier, the occupancy groups the
9
16
  * registry keys on, finish locations, dimensions, classification scales with
@@ -24,6 +31,12 @@ export class CodesClient {
24
31
  return this.#transport.get("list_code_context", undefined, signal);
25
32
  }
26
33
  /**
34
+ * THE QUESTION: "what does this space DEMAND?" — before any candidate exists.
35
+ *
36
+ * Run it before shortlisting, not after: it tells you which specification to
37
+ * filter on, which you then spend on `mg.bySpec()` or `mg.search({ filters })`.
38
+ * Use {@link CodesClient.advisory} once you hold candidates to badge.
39
+ *
27
40
  * What a space demands, before any candidate exists (GET
28
41
  * `/api/v1/codes/requirements`). State the space by explicit axes, or by a
29
42
  * `roomType` the axes are correlated from.
@@ -50,6 +63,19 @@ export class CodesClient {
50
63
  }, signal);
51
64
  }
52
65
  /**
66
+ * THE QUESTION: "do THESE candidates satisfy what this space demands?" — the
67
+ * third call, once you hold materials.
68
+ *
69
+ * Context is CONDITIONAL: supply `occupancyGroup` + `spaceType`, or a
70
+ * `roomType` for them to correlate from, or a mix — an explicit axis always
71
+ * wins, and an axis that resolves to nothing is a 400 naming it. The full rule,
72
+ * with the validator's own messages, is on {@link CodeContextRequest}.
73
+ *
74
+ * Read `summary.status` honestly: `unverified` means no comparable rating is on
75
+ * file. It is not a pass, and it is common. Use {@link CodesClient.requirements}
76
+ * when you have no candidates yet, and `mg.materialBlocks()` to see whether a
77
+ * material's fire/slip facts are missing or merely unverified.
78
+ *
53
79
  * Badge materials against the advisory building-code registry for one context
54
80
  * (POST `/api/v1/codes/advisory`, ≤ 100 ids). Each applicable requirement is
55
81
  * badged `meets` / `shortfall` / `unknown` from the material's accepted fire