@instruments/materialgraph 0.5.0 → 0.6.1

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,31 +221,78 @@ 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
297
  return this.#get("get_material_pairings", { categories: options?.categories, limit: options?.limit }, signal, { variantId });
160
298
  }
@@ -175,11 +313,20 @@ export class MaterialGraphClient {
175
313
  }, signal);
176
314
  }
177
315
  // ── Colour helpers ───────────────────────────────────────────────────────────
178
- /** The three canonical colour vocabularies with live counts (GET `/api/v1/colours/facets`). */
316
+ /**
317
+ * THE QUESTION: "what colour WORDS may I use?" — family, mood and undertone
318
+ * with live counts. Vocabulary, not values: instead use `colours.profiles()`
319
+ * for a category's palette, {@link MaterialGraphClient.byColour} to search by a
320
+ * colour, {@link MaterialGraphClient.resolveColour} to turn a NAME into a hex.
321
+ */
179
322
  colourFacets(signal) {
180
323
  return this.#get("get_colour_facets", undefined, signal);
181
324
  }
182
- /** Resolve a colour name/phrase to a hex (GET `/api/v1/search/colours/resolve`). */
325
+ /**
326
+ * THE QUESTION: "what hex is 'Hague Blue'?" — the step before
327
+ * {@link MaterialGraphClient.byColour} when you hold a name rather than a hex.
328
+ * A `null` match means unrecognised, not "no such colour".
329
+ */
183
330
  resolveColour(name, signal) {
184
331
  return this.#get("resolve_colour_name", { name }, signal);
185
332
  }
@@ -188,6 +335,11 @@ export class MaterialGraphClient {
188
335
  return this.#get("suggest_colour_names", { q, limit: options?.limit }, signal);
189
336
  }
190
337
  /**
338
+ * THE QUESTION: "what is the user starting to type?" It ranks NAMES, not
339
+ * materials — instead use {@link MaterialGraphClient.search} once they commit,
340
+ * {@link MaterialGraphClient.suggestColour} for colour names, and
341
+ * {@link MaterialGraphClient.searchVocabulary} for attribute/value tokens.
342
+ *
191
343
  * Entity typeahead across products, brands and categories
192
344
  * (GET `/api/v1/search/suggest`). Built for per-keystroke use: one indexed
193
345
  * lookup, no embedding call and no model. Every suggestion carries the
@@ -202,19 +354,53 @@ export class MaterialGraphClient {
202
354
  return this.#get("search_suggest", suggestQuery(q, options), signal);
203
355
  }
204
356
  // ── Catalog ──────────────────────────────────────────────────────────────────
205
- /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
357
+ /**
358
+ * THE QUESTION: "the user said 'fire rating' — which canonical attribute is
359
+ * that?" Free text in, canonical `attributeId`s out; pair with
360
+ * {@link MaterialGraphClient.resolveAttributeValue} for the VALUE, then spend
361
+ * the pair on {@link MaterialGraphClient.bySpec} or `search({ filters })`.
362
+ * Instead use {@link MaterialGraphClient.specificationFacets} to browse rather
363
+ * than resolve.
364
+ */
206
365
  resolveAttribute(q, options, signal) {
207
366
  return this.#get("catalog_resolve_attribute", { q, limit: options?.limit, facetableOnly: options?.facetableOnly }, signal);
208
367
  }
209
- /** Resolve a value phrase against one canonical attribute's dictionary. */
368
+ /**
369
+ * THE QUESTION: "the user said 'class A' — what canonical term is that, under
370
+ * THIS attribute?" The value half of the pair
371
+ * {@link MaterialGraphClient.resolveAttribute} starts. Read
372
+ * `resolution.recommendedAction`, not just `best`: `ambiguous` means ask.
373
+ *
374
+ * @param attributeId Canonical vocabulary, not an opaque id — discover it, do
375
+ * not invent it.
376
+ */
210
377
  resolveAttributeValue(attributeId, query, options, signal) {
211
378
  return this.#get("catalog_resolve_value", { q: query, limit: options?.limit }, signal, { attributeId });
212
379
  }
213
- /** Search canonical attributes and value-dictionary terms together. */
380
+ /**
381
+ * THE QUESTION: "is 'FloorScore' an attribute or a value, and what is it called
382
+ * here?" The catch-all when you do not know which half of an `attribute:value`
383
+ * pair a phrase belongs to. Instead use
384
+ * {@link MaterialGraphClient.resolveAttribute} /
385
+ * {@link MaterialGraphClient.resolveAttributeValue} once you do, and
386
+ * {@link MaterialGraphClient.glossary} to EXPLAIN a term rather than find it.
387
+ */
214
388
  searchVocabulary(query, options, signal) {
215
389
  return this.#get("catalog_vocabulary_search", { q: query, limit: options?.limit, kind: options?.kind }, signal);
216
390
  }
217
391
  /**
392
+ * THE QUESTION: "which materials, from ANYONE, are ASTM E84 Class A AND
393
+ * GREENGUARD-certified?" — hard specifications, no ranking, every brand at once
394
+ * whatever each called the value in source.
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 MaterialGraphClient.specificationFacets} first if you do not know
399
+ * the canonical tokens — guessing one returns an honest, empty answer.
400
+ *
401
+ * A PAGE, NOT A CORPUS, unusually so: `totalMatches` is the size of the
402
+ * returned sample under `limit`, not a corpus count. See {@link BySpecResponse}.
403
+ *
218
404
  * Find materials by canonical specification across every brand — the
219
405
  * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
220
406
  * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
@@ -229,16 +415,31 @@ export class MaterialGraphClient {
229
415
  return this.#get("catalog_by_spec", { spec: Array.isArray(spec) ? spec : [spec], limit: options?.limit }, signal);
230
416
  }
231
417
  // ── Taxonomy + meta ──────────────────────────────────────────────────────────
232
- /** The product-category tree, flattened (GET `/api/v1/taxonomy/categories`). */
418
+ /**
419
+ * THE QUESTION: "what kinds of thing exist, and what are their canonical keys?"
420
+ * The source of every `categoryKey` / product-type id the other lenses accept
421
+ * (`search({ category })`, `colours.profiles()`,
422
+ * {@link MaterialGraphClient.applicableSpecs}). Discover, never hand-write.
423
+ */
233
424
  taxonomy(signal) {
234
425
  return this.#get("get_category_taxonomy", undefined, signal);
235
426
  }
236
- /** Echo the authenticated principal and scopes (GET `/api/v1/whoami`). */
427
+ /**
428
+ * THE QUESTION: "what is this key allowed to do?" Check `scopes` before
429
+ * offering a capability: the write surfaces need `mg:ingest` / `mg:compose`,
430
+ * and `mg:admin` unlocks extra evidence fields.
431
+ */
237
432
  whoami(signal) {
238
433
  return this.#get("whoami", undefined, signal);
239
434
  }
240
435
  // ── Pairings / demand ────────────────────────────────────────────────────────
241
436
  /**
437
+ * THE QUESTION: "here is my whole scheme so far — what completes it?" The
438
+ * multi-seed form of {@link MaterialGraphClient.pairings}: lift is summed across
439
+ * seeds, so a companion suiting one pick ranks below one suiting several.
440
+ * Instead use `pairings` for a single material and `colours.palette()` when the
441
+ * scheme is COLOURS rather than materials. Check `unresolved` before presenting.
442
+ *
242
443
  * Coordinate a palette of already-chosen materials (POST
243
444
  * `/api/v1/pairings/coordinate`). Returns demand-lift-ranked companions with
244
445
  * colour swatches and an annotate-only `colourFit` (never reorders in v1).
@@ -247,13 +448,25 @@ export class MaterialGraphClient {
247
448
  return this.#post("coordinate_materials", input, signal);
248
449
  }
249
450
  /**
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`.
451
+ * THE QUESTION: "how much is this material actually specified, and what gets
452
+ * specified alongside it?" — real order behaviour, trailing 36 months.
453
+ *
454
+ * Instead use {@link MaterialGraphClient.demandBatch} for many ids (this one
455
+ * also resolves free text, which the batch deliberately does not) and
456
+ * {@link MaterialGraphClient.pairings} for a presentable rail. A null `demand`
457
+ * means UNREAD, never zero. Provide EXACTLY ONE of `variantId` or `material`.
252
458
  */
253
459
  demand(params, signal) {
254
460
  return this.#get("get_material_demand", { variantId: params.variantId, material: params.material }, signal);
255
461
  }
256
462
  /**
463
+ * THE QUESTION: "for each of these picks I already hold, how much momentum does
464
+ * it have?" Enrichment, never discovery or ranking. Instead use
465
+ * {@link MaterialGraphClient.demand} for one material named in free text and
466
+ * {@link MaterialGraphClient.search} to find candidates at all. Only
467
+ * `state: "available"` carries numbers; the other three mean "no number", never
468
+ * "no demand".
469
+ *
257
470
  * Enrich a bounded set of ALREADY-CHOSEN candidate ids with their real
258
471
  * specification demand (POST `/api/v1/demand/batch`, 1–48 ids). The bounded
259
472
  * batch complement to {@link demand}: no fuzzy resolution, ranking, or
@@ -278,20 +491,36 @@ export class MaterialGraphClient {
278
491
  }
279
492
  // ── Codes (advisory building-code compliance) ────────────────────────────────
280
493
  /**
494
+ * THE QUESTION: "may I put these materials in an A-2 corridor in Texas?"
495
+ *
281
496
  * Badge materials against the advisory building-code registry (POST
282
497
  * `/api/v1/codes/advisory`). Identical to {@link CodesClient.advisory}, kept
283
- * flat because it shipped before the `codes` namespace existed.
498
+ * flat because it shipped before the `codes` namespace existed — prefer
499
+ * `mg.codes.advisory()` in new code, and start at `mg.codes.context()` to
500
+ * discover every accepted context value. `unverified` is not a pass.
284
501
  */
285
502
  codeAdvisory(input, signal) {
286
503
  return this.codes.advisory(input, signal);
287
504
  }
288
505
  // ── Colour helpers (extract) ─────────────────────────────────────────────────
289
- /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
506
+ /**
507
+ * THE QUESTION: "what colours are in this image?" — the palette only, no
508
+ * materials. The bridge from an inspiration image into the colour lens: extract
509
+ * here, then feed the swatches to `colours.palette()` or
510
+ * {@link MaterialGraphClient.byColour}. Instead use
511
+ * {@link MaterialGraphClient.match} for the MATERIALS in the photo.
512
+ */
290
513
  extractPalette(input, signal) {
291
514
  return this.#post("extract_image_palette", input, signal);
292
515
  }
293
516
  // ── Catalog (facets / applicable specs / glossary) ───────────────────────────
294
517
  /**
518
+ * THE QUESTION: "what can I even filter on, and what values does the corpus
519
+ * 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.
523
+ *
295
524
  * Discover the live specification vocabulary (GET
296
525
  * `/api/v1/catalog/specifications/facets`) — the facetable spec attributes that
297
526
  * have real product-graph data, each with its top canonical values and the
@@ -318,20 +547,43 @@ export class MaterialGraphClient {
318
547
  attributeId,
319
548
  });
320
549
  }
321
- /** The specs a product type COULD carry (GET `/api/v1/taxonomy/categories/{key}/specs`). */
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
+ */
322
558
  applicableSpecs(key, signal) {
323
559
  return this.#get("get_applicable_specs", undefined, signal, { key });
324
560
  }
325
- /** Resolve a term to its meaning in the schema (GET `/api/v1/glossary/{term}`). */
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
+ */
326
567
  glossary(term, signal) {
327
568
  return this.#get("explain_material_term", undefined, signal, { term });
328
569
  }
329
570
  // ── Brands ───────────────────────────────────────────────────────────────────
330
- /** List registered brands (GET `/api/v1/brands`). */
571
+ /**
572
+ * THE QUESTION: "whose catalogs are in here, and how big is each?" The SDK's
573
+ * one OFFSET-paginated surface: read `meta.total` for the collection size and
574
+ * `meta.hasMore` to keep walking. Instead use {@link MaterialGraphClient.brand}
575
+ * for one brand's profile and trade signals.
576
+ */
331
577
  brands(options = {}, signal) {
332
578
  return this.#get("list_brands", { q: options.q, limit: options.limit, offset: options.offset }, signal);
333
579
  }
334
- /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{brandSlug}`). */
580
+ /**
581
+ * THE QUESTION: "who is this brand, and what do they claim?" — profile plus the
582
+ * trade/spec signals ON RECORD. `trade` is keyed by attribute id and an ABSENT
583
+ * key means no value is held — never infer `false`. Instead use
584
+ * {@link MaterialGraphClient.search} to find their materials; this describes the
585
+ * company, not the catalog.
586
+ */
335
587
  brand(brandSlug, options, signal) {
336
588
  return this.#get("get_brand_profile", { q: options?.q }, signal, {
337
589
  brandSlug,
@@ -358,15 +610,33 @@ export class MaterialGraphClient {
358
610
  sourceMaterialBatch(batchId, signal) {
359
611
  return getSourceMaterialBatchCall(this.#transport, batchId, signal);
360
612
  }
361
- /** List invocable agents, workflows, and tools (GET `/api/v1/capabilities`). */
613
+ /**
614
+ * THE QUESTION: "what can the hosted agentic surface be asked to DO?" Start
615
+ * here before {@link MaterialGraphClient.createRun}: each capability carries its
616
+ * live input/output schemas, scopes and side-effect class, so nothing about a
617
+ * Run need be hardcoded.
618
+ */
362
619
  capabilities(signal) {
363
620
  return capabilitiesRun(this.#transport, signal);
364
621
  }
365
- /** Start an asynchronous capability Run (POST `/api/v1/runs`). */
622
+ /**
623
+ * THE QUESTION: "do a whole job for me" — the ASYNC lens. Everything else here
624
+ * answers in one round trip; a Run may suspend for human input and finish
625
+ * minutes later. Follow with {@link MaterialGraphClient.streamRunEvents} (live)
626
+ * or {@link MaterialGraphClient.run} (poll); resume via
627
+ * {@link MaterialGraphClient.resumeRun}; pick the capability from
628
+ * {@link MaterialGraphClient.capabilities}. Pass `idempotencyKey` so a retry
629
+ * replays rather than duplicates.
630
+ */
366
631
  createRun(body, signal) {
367
632
  return createRun(this.#transport, body, signal);
368
633
  }
369
- /** Get a Run's current state (GET `/api/v1/runs/{runId}`). */
634
+ /**
635
+ * THE QUESTION: "where has this Run got to?" — the caller-owned, redacted
636
+ * ledger record. Polling; prefer {@link MaterialGraphClient.streamRunEvents}
637
+ * when a human is waiting. `suspended` is not a failure — it means the Run is
638
+ * asking you something (see {@link MaterialGraphClient.resumeRun}).
639
+ */
370
640
  run(runId, signal) {
371
641
  return getRun(this.#transport, runId, signal);
372
642
  }
@@ -390,7 +660,11 @@ export class MaterialGraphClient {
390
660
  resumeRun(runId, resumeData, signalOrOptions) {
391
661
  return resumeRun(this.#transport, runId, resumeData, signalOrOptions);
392
662
  }
393
- /** Request durable Run cancellation (POST `/api/v1/runs/{runId}/cancel`). */
663
+ /**
664
+ * THE QUESTION: "stop this Run." Requests DURABLE cancellation — the Run
665
+ * acknowledges and unwinds; the call returning does not mean it has stopped.
666
+ * Poll {@link MaterialGraphClient.run} for the terminal state.
667
+ */
394
668
  cancelRun(runId, signal) {
395
669
  return cancelRun(this.#transport, runId, signal);
396
670
  }
@@ -421,7 +695,11 @@ export class MaterialGraphClient {
421
695
  createProject(body = {}, signal) {
422
696
  return createProjectCall(this.#transport, body, signal);
423
697
  }
424
- /** Read a project's assembled state (GET `/api/v1/projects/{projectId}`). */
698
+ /**
699
+ * THE QUESTION: "what has happened on this project?" — the append-only event log
700
+ * FOLDED into a read model, plus the raw `events`. Read the folded collections;
701
+ * the log is there for audit.
702
+ */
425
703
  getProject(projectId, signal) {
426
704
  return getProjectCall(this.#transport, projectId, signal);
427
705
  }
@@ -432,7 +710,10 @@ export class MaterialGraphClient {
432
710
  appendProjectEvent(projectId, body, signal) {
433
711
  return appendProjectEventCall(this.#transport, projectId, body, signal);
434
712
  }
435
- /** List the caller's own projects, newest first (GET `/api/v1/projects`). */
713
+ /**
714
+ * THE QUESTION: "what Boxes do I have?" Scoped to the authenticated principal —
715
+ * another key's projects are invisible, not forbidden.
716
+ */
436
717
  listProjects(signal) {
437
718
  return listProjectsCall(this.#transport, signal);
438
719
  }
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