@instruments/materialgraph 0.1.0 → 0.2.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.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { type RateLimit } from "./errors.js";
2
+ import type { CapabilityManifestResponse, CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, PublicRunResponse, RunEvent, SourceMaterialBatchReceipt, StreamRunEventsOptions, UploadSourceMaterialInput } from "./run-types.js";
3
+ import type { AppendProjectEventRequest, CreateProjectRequest, ProjectEventReceipt, ProjectListResponse, ProjectState } from "./project-types.js";
4
+ import type { VarySchemesRequest, VarySchemesResponse } from "./composition-types.js";
5
+ import type { CodeAdvisoryInput, CodeAdvisoryResponse } from "./codes-types.js";
2
6
  import type { BrandCatalogListPage, BrandsOptions } from "./brand-types.js";
3
7
  import type { MaterialAssetProjection } from "./material-asset-types.js";
4
8
  import type { MaterialBlockProjection } from "./material-block-types.js";
5
9
  import type { SchemaResource, SchemaResourceResponse } from "./schema-resource-types.js";
6
- import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, ByColourParams, ByColourResponse, BrowseParams, BrowseResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, MatchInput, MatchResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, TaxonomyResponse, VocabularySearchOptions, VocabularySearchResponse, WhoamiResponse } from "./types.js";
10
+ import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, ByColourParams, ByColourResponse, BrowseParams, BrowseResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandBatchInput, DemandBatchResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, MatchInput, MatchResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, TaxonomyResponse, VocabularySearchOptions, VocabularySearchResponse, WhoamiResponse } from "./types.js";
7
11
  export declare const DEFAULT_BASE_URL = "https://beta.materialgraph.com";
8
12
  export declare const MAX_BATCH_IDS = 100;
9
13
  export interface MaterialGraphClientOptions {
@@ -24,6 +28,17 @@ export declare class MaterialGraphClient {
24
28
  readonly browse: BrowseNamespace;
25
29
  constructor(options: MaterialGraphClientOptions);
26
30
  get lastRateLimit(): RateLimit | null;
31
+ /**
32
+ * The full search primitive (POST `/api/v1/search`). Layer a hard
33
+ * specification gate onto a ranked (colour / visual / lexical / seed) search
34
+ * via `input.filters` — each entry is a typed {@link SearchFilter} criterion:
35
+ * a term match (`{ attributeId, term }`), a presence check
36
+ * (`{ attributeId, hasValue: true }`), or a certification programme
37
+ * (`{ attributeId: "certification_programme", term }`). This is the seam for
38
+ * "a GREENGUARD-certified alternative to THIS material" (seed by `similarTo`,
39
+ * gate by `filters`) or "the same fire class, with an EPD on record". Build the
40
+ * filters with the `certificationProgramme` / `specTerm` / `hasSpec` helpers.
41
+ */
27
42
  search(input: SearchInput, signal?: AbortSignal): Promise<SearchResponse>;
28
43
  searchGet(params: SearchGetParams, signal?: AbortSignal): Promise<SearchResponse>;
29
44
  match(input: MatchInput, signal?: AbortSignal): Promise<MatchResponse>;
@@ -83,8 +98,15 @@ export declare class MaterialGraphClient {
83
98
  /** Search canonical attributes and value-dictionary terms together. */
84
99
  searchVocabulary(query: string, options?: VocabularySearchOptions, signal?: AbortSignal): Promise<VocabularySearchResponse>;
85
100
  /**
86
- * Find materials by canonical specification (GET `/api/v1/search/specifications`).
87
- * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL.
101
+ * Find materials by canonical specification across every brand — the
102
+ * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
103
+ * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
104
+ * them, and each match carries per-criterion trust (`confidence` +
105
+ * `sourceBasis`). Answers "which materials, from anyone, are ASTM E84 Class A
106
+ * AND GREENGUARD-certified?" Build the strings with `specCriterion` /
107
+ * `certificationProgrammeCriterion` (e.g.
108
+ * `certificationProgrammeCriterion("GREENGUARD Gold")`); discover the queryable
109
+ * vocabulary with {@link specificationFacets} and {@link attributeValues}.
88
110
  */
89
111
  bySpec(spec: string | string[], options?: BySpecOptions, signal?: AbortSignal): Promise<BySpecResponse>;
90
112
  /** The product-category tree, flattened (GET `/api/v1/taxonomy/categories`). */
@@ -102,11 +124,72 @@ export declare class MaterialGraphClient {
102
124
  * `/api/v1/demand`). Provide EXACTLY ONE of `variantId` or `material`.
103
125
  */
104
126
  demand(params: DemandParams, signal?: AbortSignal): Promise<DemandResponse>;
127
+ /**
128
+ * Enrich a bounded set of ALREADY-CHOSEN candidate ids with their real
129
+ * specification demand (POST `/api/v1/demand/batch`, 1–48 ids). The bounded
130
+ * batch complement to {@link demand}: no fuzzy resolution, ranking, or
131
+ * reordering — each id yields one discriminated `state` (`available` /
132
+ * `insufficient_data` / `no_signal` / `unknown_variant`) in input order and
133
+ * cardinality. This is the Desk's "show demand momentum for every pick in this
134
+ * scheme" read; an `mg:admin` key additionally sees per-result `evidence`.
135
+ *
136
+ * @example
137
+ * // Rank a shortlist by real specification momentum before locking the scheme.
138
+ * const { results } = await mg.demandBatch({
139
+ * variantIds: scheme.picks.map((p) => p.variantId),
140
+ * signal: "sampled",
141
+ * context: { projectType: "hospitality" },
142
+ * });
143
+ * const rising = results.filter(
144
+ * (r) => r.state === "available" && r.momentum.state === "rising",
145
+ * );
146
+ */
147
+ demandBatch(input: DemandBatchInput, signal?: AbortSignal): Promise<DemandBatchResponse>;
148
+ /**
149
+ * Badge a set of materials against the advisory building-code registry for one
150
+ * (jurisdiction, occupancy, space, sprinklered) context (POST
151
+ * `/api/v1/codes/advisory`, ≤ 48 ids). Every applicable requirement is badged
152
+ * `meets` / `shortfall` / `unknown` from the material's accepted fire and
153
+ * slip-resistance facts, in input order. ADVISORY ONLY — every verdict and the
154
+ * envelope carry the AHJ caveat; this is model code as adopted, never a legal
155
+ * determination.
156
+ *
157
+ * @example
158
+ * // Badge this scheme's picks for a Texas hospitality fire-code review.
159
+ * const advisory = await mg.codeAdvisory({
160
+ * variantIds: scheme.picks.map((p) => p.variantId),
161
+ * jurisdictionId: "us-tx",
162
+ * occupancyGroup: "A-2", // assembly — restaurant / bar
163
+ * spaceType: "corridor",
164
+ * sprinklered: true,
165
+ * });
166
+ * const shortfalls = advisory.results.filter((r) =>
167
+ * r.requirements.some((req) => req.verdict === "shortfall"),
168
+ * );
169
+ */
170
+ codeAdvisory(input: CodeAdvisoryInput, signal?: AbortSignal): Promise<CodeAdvisoryResponse>;
105
171
  /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
106
172
  extractPalette(input: ExtractPaletteInput, signal?: AbortSignal): Promise<ExtractPaletteResponse>;
107
- /** Discover the live specification-facet collection. */
173
+ /**
174
+ * Discover the live specification vocabulary (GET
175
+ * `/api/v1/catalog/specifications/facets`) — the facetable spec attributes that
176
+ * have real product-graph data, each with its top canonical values and the
177
+ * variants/brands behind them. This is where a caller LEARNS that the data
178
+ * exists: LEED, GREENGUARD, ASTM E84 Class A, FloorScore, EPDs all surface here
179
+ * with counts. Includes the virtual `certification_programme` facet (its values
180
+ * are programme names). Learn an `attribute:value` here, then reuse it as a
181
+ * {@link bySpec} criterion or a {@link search} `filters` entry. NOTE: continuous
182
+ * numeric specs (e.g. `slip_resistance_dcof`, a 0–1 DCOF range) are NOT
183
+ * token-facetable and do not appear here — read those per-attribute.
184
+ */
108
185
  specificationFacets(signal?: AbortSignal): Promise<SpecFacetsListResponse>;
109
- /** List the live values for one canonical attribute. */
186
+ /**
187
+ * List the live value distribution for ONE canonical attribute (GET
188
+ * `/api/v1/catalog/attributes/{attributeId}/values`), ordered by cross-brand
189
+ * reach with variant + brand counts. The drill-in companion to
190
+ * {@link specificationFacets} — e.g. every certification programme on record,
191
+ * or every fire classification and how many variants carry each.
192
+ */
110
193
  attributeValues(attributeId: string, signal?: AbortSignal): Promise<SpecFacetsDistributionResponse>;
111
194
  /** The specs a product type COULD carry (GET `/api/v1/taxonomy/categories/{key}/specs`). */
112
195
  applicableSpecs(key: string, signal?: AbortSignal): Promise<ApplicableSpecsResponse>;
@@ -114,6 +197,57 @@ export declare class MaterialGraphClient {
114
197
  glossary(term: string, signal?: AbortSignal): Promise<GlossaryTermResponse>;
115
198
  /** List registered brands (GET `/api/v1/brands`). */
116
199
  brands(options?: BrandsOptions, signal?: AbortSignal): Promise<BrandCatalogListPage>;
117
- /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{slug}`). */
118
- brand(slug: string, options?: BrandOptions, signal?: AbortSignal): Promise<BrandProfileResponse>;
200
+ /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{brandSlug}`). */
201
+ brand(brandSlug: string, options?: BrandOptions, signal?: AbortSignal): Promise<BrandProfileResponse>;
202
+ /**
203
+ * Upload one atomic source-material batch (POST `/api/v1/source-material`,
204
+ * multipart). Pass a heterogeneous pile via `{ files }` (any mix of PDF,
205
+ * text/markdown, and images — the server classifies each) or the legacy
206
+ * `{ brief, references }` shape. Requires the `mg:ingest` scope.
207
+ */
208
+ uploadSourceMaterial(input: UploadSourceMaterialInput, signal?: AbortSignal): Promise<SourceMaterialBatchReceipt>;
209
+ /** List invocable agents, workflows, and tools (GET `/api/v1/capabilities`). */
210
+ capabilities(signal?: AbortSignal): Promise<CapabilityManifestResponse>;
211
+ /** Start an asynchronous capability Run (POST `/api/v1/runs`). */
212
+ createRun(body: CreatePublicRunRequest, signal?: AbortSignal): Promise<CreatePublicRunResponse>;
213
+ /** Get a Run's current state (GET `/api/v1/runs/{runId}`). */
214
+ run(runId: string, signal?: AbortSignal): Promise<PublicRunResponse>;
215
+ /** Resume a suspended Run (POST `/api/v1/runs/{runId}/resume`). */
216
+ resumeRun(runId: string, resumeData: unknown, signal?: AbortSignal): Promise<PublicRunResponse>;
217
+ /** Request durable Run cancellation (POST `/api/v1/runs/{runId}/cancel`). */
218
+ cancelRun(runId: string, signal?: AbortSignal): Promise<PublicRunResponse>;
219
+ /**
220
+ * Derive an "option B" scheme from a succeeded compose Run along one
221
+ * constraint axis (POST `/api/v1/schemes/vary`). Deterministic and
222
+ * synchronous — the response IS the hydrated scheme (no Run envelope), stamped
223
+ * with axis + source provenance whose `notes` are the variation's receipts.
224
+ * Requires the `mg:read` scope.
225
+ */
226
+ varyScheme(body: VarySchemesRequest, signal?: AbortSignal): Promise<VarySchemesResponse>;
227
+ /**
228
+ * Stream ordered Run events as an async iterator (GET
229
+ * `/api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or the
230
+ * signal aborts; pass `after` to resume from the last observed sequence.
231
+ */
232
+ streamRunEvents(runId: string, options?: StreamRunEventsOptions): AsyncGenerator<RunEvent>;
233
+ /**
234
+ * Create a persistent project owned by the caller (POST `/api/v1/projects`).
235
+ * Returns the freshly assembled state (an empty event log). Requires the
236
+ * `mg:ingest` scope.
237
+ */
238
+ createProject(body?: CreateProjectRequest, signal?: AbortSignal): Promise<ProjectState>;
239
+ /** Read a project's assembled state (GET `/api/v1/projects/{projectId}`). */
240
+ getProject(projectId: string, signal?: AbortSignal): Promise<ProjectState>;
241
+ /**
242
+ * Append one event to a project's append-only process trail (POST
243
+ * `/api/v1/projects/{projectId}/events`). Requires the `mg:ingest` scope.
244
+ */
245
+ appendProjectEvent(projectId: string, body: AppendProjectEventRequest, signal?: AbortSignal): Promise<ProjectEventReceipt>;
246
+ /** List the caller's own projects, newest first (GET `/api/v1/projects`). */
247
+ listProjects(signal?: AbortSignal): Promise<ProjectListResponse>;
248
+ /**
249
+ * Collect Run events until a terminal event, the deadline, or the stream ends
250
+ * — the bounded events-log pattern (a single pass over {@link streamRunEvents}).
251
+ */
252
+ drainRunEvents(runId: string, options: DrainRunEventsOptions): Promise<RunEvent[]>;
119
253
  }
package/client.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { MaterialGraphError } from "./errors.js";
2
- import { SDK_OPERATIONS } from "./operations.js";
2
+ import { HttpTransport } from "./http-transport.js";
3
+ import { findSdkOperation } from "./operations.js";
4
+ import { cancelRun, capabilities as capabilitiesRun, createRun, drainRunEvents, getRun, resumeRun, streamRunEvents, uploadSourceMaterial, } from "./runs.js";
5
+ import { appendProjectEvent as appendProjectEventCall, createProject as createProjectCall, getProject as getProjectCall, listProjects as listProjectsCall, } from "./projects.js";
3
6
  export const DEFAULT_BASE_URL = "https://beta.materialgraph.com";
4
7
  export const MAX_BATCH_IDS = 100;
5
8
  const SCHEMA_RESOURCE_OPERATION_IDS = {
@@ -14,145 +17,50 @@ const SCHEMA_RESOURCE_OPERATION_IDS = {
14
17
  mappings: "list_schema_mappings",
15
18
  };
16
19
  function operation(operationId) {
17
- const match = SDK_OPERATIONS.find((candidate) => candidate.operationId === operationId);
20
+ const match = findSdkOperation(operationId);
18
21
  if (!match) {
19
22
  throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
20
23
  }
21
24
  return match;
22
25
  }
23
- function operationPath(sdkOperation, pathParams = {}) {
24
- return Object.entries(pathParams).reduce((path, [name, value]) => path.replace(`{${name}}`, encodeURIComponent(value)), sdkOperation.path);
25
- }
26
- function parseIntOrNull(value) {
27
- if (value === null)
28
- return null;
29
- const n = Number.parseInt(value, 10);
30
- return Number.isNaN(n) ? null : n;
31
- }
32
- function parseRateLimit(headers) {
33
- const limit = parseIntOrNull(headers.get("x-ratelimit-limit"));
34
- const remaining = parseIntOrNull(headers.get("x-ratelimit-remaining"));
35
- const reset = parseIntOrNull(headers.get("x-ratelimit-reset"));
36
- const retryAfter = parseIntOrNull(headers.get("retry-after"));
37
- if (limit === null && remaining === null && reset === null && retryAfter === null) {
38
- return null;
39
- }
40
- return { limit, remaining, reset, retryAfter };
41
- }
42
26
  export class MaterialGraphClient {
43
27
  baseUrl;
44
28
  browse;
45
- #apiKey;
46
- #fetch;
47
- #lastRateLimit = null;
29
+ #transport;
48
30
  constructor(options) {
49
31
  if (!options.apiKey) {
50
32
  throw MaterialGraphError.unexpected("MaterialGraphClient: `apiKey` is required.");
51
33
  }
52
- this.#apiKey = options.apiKey;
53
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
54
- this.#fetch = options.fetch ?? globalThis.fetch;
34
+ this.#transport = new HttpTransport({
35
+ apiKey: options.apiKey,
36
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
37
+ fetch: options.fetch ?? globalThis.fetch,
38
+ });
39
+ this.baseUrl = this.#transport.baseUrl;
55
40
  const browse = (params, signal) => this.#browsePage(params, signal);
56
41
  browse.pages = (params, signal) => this.#browsePages(params, signal);
57
42
  this.browse = browse;
58
43
  }
59
44
  get lastRateLimit() {
60
- return this.#lastRateLimit;
61
- }
62
- #buildUrl(path, query) {
63
- const url = new URL(`${this.baseUrl}${path}`);
64
- if (query) {
65
- for (const [key, value] of Object.entries(query)) {
66
- if (value === undefined || value === null)
67
- continue;
68
- if (Array.isArray(value)) {
69
- for (const item of value) {
70
- if (item !== undefined && item !== null) {
71
- url.searchParams.append(key, String(item));
72
- }
73
- }
74
- }
75
- else {
76
- url.searchParams.append(key, String(value));
77
- }
78
- }
79
- }
80
- return url.toString();
81
- }
82
- async #request(sdkOperation, options = {}) {
83
- const path = operationPath(sdkOperation, options.pathParams);
84
- const url = this.#buildUrl(path, options.query);
85
- const requestUrl = new URL(url);
86
- const requestMetadata = {
87
- method: sdkOperation.method,
88
- origin: requestUrl.origin,
89
- path: requestUrl.pathname,
90
- };
91
- const headers = {
92
- Authorization: `Bearer ${this.#apiKey}`,
93
- Accept: "application/json",
94
- };
95
- const hasBody = options.body !== undefined;
96
- if (hasBody)
97
- headers["Content-Type"] = "application/json";
98
- let response;
99
- try {
100
- response = await this.#fetch(url, {
101
- method: sdkOperation.method,
102
- headers,
103
- body: hasBody ? JSON.stringify(options.body) : undefined,
104
- signal: options.signal,
105
- });
106
- }
107
- catch (cause) {
108
- throw MaterialGraphError.fromTransport({
109
- ...requestMetadata,
110
- cause,
111
- callerSignal: options.signal,
112
- });
113
- }
114
- const rateLimit = parseRateLimit(response.headers);
115
- this.#lastRateLimit = rateLimit;
116
- let text;
117
- try {
118
- text = await response.text();
119
- }
120
- catch (cause) {
121
- throw MaterialGraphError.fromTransport({
122
- ...requestMetadata,
123
- cause,
124
- callerSignal: options.signal,
125
- });
126
- }
127
- let parsed;
128
- if (text.length > 0) {
129
- try {
130
- parsed = JSON.parse(text);
131
- }
132
- catch {
133
- parsed = text;
134
- }
135
- }
136
- if (!response.ok) {
137
- const querySecrets = [...requestUrl.searchParams.values()];
138
- throw MaterialGraphError.fromResponse({
139
- ...requestMetadata,
140
- status: response.status,
141
- body: parsed,
142
- responseText: text,
143
- rateLimit,
144
- apiKey: this.#apiKey,
145
- queryValues: querySecrets,
146
- });
147
- }
148
- return parsed;
45
+ return this.#transport.lastRateLimit;
149
46
  }
150
47
  #get(operationId, query, signal, pathParams) {
151
- return this.#request(operation(operationId), { query, signal, pathParams });
48
+ return this.#transport.requestJson(operation(operationId), { query, signal, pathParams });
152
49
  }
153
50
  #post(operationId, body, signal) {
154
- return this.#request(operation(operationId), { body, signal });
51
+ return this.#transport.requestJson(operation(operationId), { body, signal });
155
52
  }
53
+ /**
54
+ * The full search primitive (POST `/api/v1/search`). Layer a hard
55
+ * specification gate onto a ranked (colour / visual / lexical / seed) search
56
+ * via `input.filters` — each entry is a typed {@link SearchFilter} criterion:
57
+ * a term match (`{ attributeId, term }`), a presence check
58
+ * (`{ attributeId, hasValue: true }`), or a certification programme
59
+ * (`{ attributeId: "certification_programme", term }`). This is the seam for
60
+ * "a GREENGUARD-certified alternative to THIS material" (seed by `similarTo`,
61
+ * gate by `filters`) or "the same fire class, with an EPD on record". Build the
62
+ * filters with the `certificationProgramme` / `specTerm` / `hasSpec` helpers.
63
+ */
156
64
  search(input, signal) {
157
65
  return this.#post("search_materials_post", input, signal);
158
66
  }
@@ -264,8 +172,15 @@ export class MaterialGraphClient {
264
172
  return this.#get("catalog_vocabulary_search", { q: query, limit: options?.limit, kind: options?.kind }, signal);
265
173
  }
266
174
  /**
267
- * Find materials by canonical specification (GET `/api/v1/search/specifications`).
268
- * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL.
175
+ * Find materials by canonical specification across every brand — the
176
+ * value-anchored, spec-only primitive (GET `/api/v1/search/specifications`).
177
+ * Each `spec` is an `attribute:value` criterion; a variant must satisfy ALL of
178
+ * them, and each match carries per-criterion trust (`confidence` +
179
+ * `sourceBasis`). Answers "which materials, from anyone, are ASTM E84 Class A
180
+ * AND GREENGUARD-certified?" Build the strings with `specCriterion` /
181
+ * `certificationProgrammeCriterion` (e.g.
182
+ * `certificationProgrammeCriterion("GREENGUARD Gold")`); discover the queryable
183
+ * vocabulary with {@link specificationFacets} and {@link attributeValues}.
269
184
  */
270
185
  bySpec(spec, options, signal) {
271
186
  return this.#get("catalog_by_spec", { spec: Array.isArray(spec) ? spec : [spec], limit: options?.limit }, signal);
@@ -295,17 +210,83 @@ export class MaterialGraphClient {
295
210
  demand(params, signal) {
296
211
  return this.#get("get_material_demand", { variantId: params.variantId, material: params.material }, signal);
297
212
  }
213
+ /**
214
+ * Enrich a bounded set of ALREADY-CHOSEN candidate ids with their real
215
+ * specification demand (POST `/api/v1/demand/batch`, 1–48 ids). The bounded
216
+ * batch complement to {@link demand}: no fuzzy resolution, ranking, or
217
+ * reordering — each id yields one discriminated `state` (`available` /
218
+ * `insufficient_data` / `no_signal` / `unknown_variant`) in input order and
219
+ * cardinality. This is the Desk's "show demand momentum for every pick in this
220
+ * scheme" read; an `mg:admin` key additionally sees per-result `evidence`.
221
+ *
222
+ * @example
223
+ * // Rank a shortlist by real specification momentum before locking the scheme.
224
+ * const { results } = await mg.demandBatch({
225
+ * variantIds: scheme.picks.map((p) => p.variantId),
226
+ * signal: "sampled",
227
+ * context: { projectType: "hospitality" },
228
+ * });
229
+ * const rising = results.filter(
230
+ * (r) => r.state === "available" && r.momentum.state === "rising",
231
+ * );
232
+ */
233
+ demandBatch(input, signal) {
234
+ return this.#post("get_demand_batch", input, signal);
235
+ }
236
+ // ── Codes (advisory building-code compliance) ────────────────────────────────
237
+ /**
238
+ * Badge a set of materials against the advisory building-code registry for one
239
+ * (jurisdiction, occupancy, space, sprinklered) context (POST
240
+ * `/api/v1/codes/advisory`, ≤ 48 ids). Every applicable requirement is badged
241
+ * `meets` / `shortfall` / `unknown` from the material's accepted fire and
242
+ * slip-resistance facts, in input order. ADVISORY ONLY — every verdict and the
243
+ * envelope carry the AHJ caveat; this is model code as adopted, never a legal
244
+ * determination.
245
+ *
246
+ * @example
247
+ * // Badge this scheme's picks for a Texas hospitality fire-code review.
248
+ * const advisory = await mg.codeAdvisory({
249
+ * variantIds: scheme.picks.map((p) => p.variantId),
250
+ * jurisdictionId: "us-tx",
251
+ * occupancyGroup: "A-2", // assembly — restaurant / bar
252
+ * spaceType: "corridor",
253
+ * sprinklered: true,
254
+ * });
255
+ * const shortfalls = advisory.results.filter((r) =>
256
+ * r.requirements.some((req) => req.verdict === "shortfall"),
257
+ * );
258
+ */
259
+ codeAdvisory(input, signal) {
260
+ return this.#post("resolveCodeAdvisory", input, signal);
261
+ }
298
262
  // ── Colour helpers (extract) ─────────────────────────────────────────────────
299
263
  /** Extract a named colour palette from an uploaded image (POST `/api/v1/search/palettes/extract`). */
300
264
  extractPalette(input, signal) {
301
265
  return this.#post("extract_image_palette", input, signal);
302
266
  }
303
267
  // ── Catalog (facets / applicable specs / glossary) ───────────────────────────
304
- /** Discover the live specification-facet collection. */
268
+ /**
269
+ * Discover the live specification vocabulary (GET
270
+ * `/api/v1/catalog/specifications/facets`) — the facetable spec attributes that
271
+ * have real product-graph data, each with its top canonical values and the
272
+ * variants/brands behind them. This is where a caller LEARNS that the data
273
+ * exists: LEED, GREENGUARD, ASTM E84 Class A, FloorScore, EPDs all surface here
274
+ * with counts. Includes the virtual `certification_programme` facet (its values
275
+ * are programme names). Learn an `attribute:value` here, then reuse it as a
276
+ * {@link bySpec} criterion or a {@link search} `filters` entry. NOTE: continuous
277
+ * numeric specs (e.g. `slip_resistance_dcof`, a 0–1 DCOF range) are NOT
278
+ * token-facetable and do not appear here — read those per-attribute.
279
+ */
305
280
  specificationFacets(signal) {
306
281
  return this.#get("list_spec_facets", undefined, signal);
307
282
  }
308
- /** List the live values for one canonical attribute. */
283
+ /**
284
+ * List the live value distribution for ONE canonical attribute (GET
285
+ * `/api/v1/catalog/attributes/{attributeId}/values`), ordered by cross-brand
286
+ * reach with variant + brand counts. The drill-in companion to
287
+ * {@link specificationFacets} — e.g. every certification programme on record,
288
+ * or every fire classification and how many variants carry each.
289
+ */
309
290
  attributeValues(attributeId, signal) {
310
291
  return this.#get("list_attribute_values", undefined, signal, {
311
292
  attributeId,
@@ -324,10 +305,89 @@ export class MaterialGraphClient {
324
305
  brands(options = {}, signal) {
325
306
  return this.#get("list_brands", { q: options.q, limit: options.limit, offset: options.offset }, signal);
326
307
  }
327
- /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{slug}`). */
328
- brand(slug, options, signal) {
308
+ /** Resolve a brand and return its profile + trade signals (GET `/api/v1/brands/{brandSlug}`). */
309
+ brand(brandSlug, options, signal) {
329
310
  return this.#get("get_brand_profile", { q: options?.q }, signal, {
330
- slug,
311
+ brandSlug,
331
312
  });
332
313
  }
314
+ // ── Runs (the hosted agentic surface) ────────────────────────────────────────
315
+ /**
316
+ * Upload one atomic source-material batch (POST `/api/v1/source-material`,
317
+ * multipart). Pass a heterogeneous pile via `{ files }` (any mix of PDF,
318
+ * text/markdown, and images — the server classifies each) or the legacy
319
+ * `{ brief, references }` shape. Requires the `mg:ingest` scope.
320
+ */
321
+ uploadSourceMaterial(input, signal) {
322
+ return uploadSourceMaterial(this.#transport, input, signal);
323
+ }
324
+ /** List invocable agents, workflows, and tools (GET `/api/v1/capabilities`). */
325
+ capabilities(signal) {
326
+ return capabilitiesRun(this.#transport, signal);
327
+ }
328
+ /** Start an asynchronous capability Run (POST `/api/v1/runs`). */
329
+ createRun(body, signal) {
330
+ return createRun(this.#transport, body, signal);
331
+ }
332
+ /** Get a Run's current state (GET `/api/v1/runs/{runId}`). */
333
+ run(runId, signal) {
334
+ return getRun(this.#transport, runId, signal);
335
+ }
336
+ /** Resume a suspended Run (POST `/api/v1/runs/{runId}/resume`). */
337
+ resumeRun(runId, resumeData, signal) {
338
+ return resumeRun(this.#transport, runId, resumeData, signal);
339
+ }
340
+ /** Request durable Run cancellation (POST `/api/v1/runs/{runId}/cancel`). */
341
+ cancelRun(runId, signal) {
342
+ return cancelRun(this.#transport, runId, signal);
343
+ }
344
+ /**
345
+ * Derive an "option B" scheme from a succeeded compose Run along one
346
+ * constraint axis (POST `/api/v1/schemes/vary`). Deterministic and
347
+ * synchronous — the response IS the hydrated scheme (no Run envelope), stamped
348
+ * with axis + source provenance whose `notes` are the variation's receipts.
349
+ * Requires the `mg:read` scope.
350
+ */
351
+ varyScheme(body, signal) {
352
+ return this.#post("vary_schemes", body, signal);
353
+ }
354
+ /**
355
+ * Stream ordered Run events as an async iterator (GET
356
+ * `/api/v1/runs/{runId}/events`, SSE). Iterates until the stream ends or the
357
+ * signal aborts; pass `after` to resume from the last observed sequence.
358
+ */
359
+ streamRunEvents(runId, options) {
360
+ return streamRunEvents(this.#transport, runId, options);
361
+ }
362
+ // ── Projects (the persistent Material Box entity) ────────────────────────────
363
+ /**
364
+ * Create a persistent project owned by the caller (POST `/api/v1/projects`).
365
+ * Returns the freshly assembled state (an empty event log). Requires the
366
+ * `mg:ingest` scope.
367
+ */
368
+ createProject(body = {}, signal) {
369
+ return createProjectCall(this.#transport, body, signal);
370
+ }
371
+ /** Read a project's assembled state (GET `/api/v1/projects/{projectId}`). */
372
+ getProject(projectId, signal) {
373
+ return getProjectCall(this.#transport, projectId, signal);
374
+ }
375
+ /**
376
+ * Append one event to a project's append-only process trail (POST
377
+ * `/api/v1/projects/{projectId}/events`). Requires the `mg:ingest` scope.
378
+ */
379
+ appendProjectEvent(projectId, body, signal) {
380
+ return appendProjectEventCall(this.#transport, projectId, body, signal);
381
+ }
382
+ /** List the caller's own projects, newest first (GET `/api/v1/projects`). */
383
+ listProjects(signal) {
384
+ return listProjectsCall(this.#transport, signal);
385
+ }
386
+ /**
387
+ * Collect Run events until a terminal event, the deadline, or the stream ends
388
+ * — the bounded events-log pattern (a single pass over {@link streamRunEvents}).
389
+ */
390
+ drainRunEvents(runId, options) {
391
+ return drainRunEvents(this.#transport, runId, options);
392
+ }
333
393
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Hand-mirrored wire types for the advisory building-code surface
3
+ * (`POST /api/v1/codes/advisory`, MG-433).
4
+ *
5
+ * Like the rest of the SDK these interfaces have NO runtime dependency on any
6
+ * `@materialgraph/*` package — they mirror the `codeAdvisoryInputSchema` /
7
+ * `codeAdvisoryResultSchema` Zod contracts the route is built from. Response
8
+ * enums follow the SDK's TOLERANT-PARSING contract (widened with `OpenEnum` so a
9
+ * new server value still type-checks); `requirement.scale` is an open IBC-scale
10
+ * id and `caveat` is the AHJ advisory sentence, both read as plain strings.
11
+ *
12
+ * ADVISORY ONLY: every verdict and the envelope carry the AHJ caveat — this is
13
+ * model code as adopted, never a legal determination.
14
+ */
15
+ import type { OpenEnum } from "./types.js";
16
+ /**
17
+ * Badge a bounded set of materials against the code-requirement registry for one
18
+ * (jurisdiction, occupancy, space, sprinklered) context. `results` preserve
19
+ * input order and cardinality (duplicate ids are echoed).
20
+ */
21
+ export interface CodeAdvisoryInput {
22
+ /** 1–48 opaque variant ids to badge, echoed back in `results` in this exact order. */
23
+ variantIds: string[];
24
+ /** A seeded jurisdiction id (e.g. `"us-tx"`); an unknown id is a 400. */
25
+ jurisdictionId: string;
26
+ /** The IBC occupancy group for the space (e.g. `"I-2"` hospital, `"A-2"` restaurant). */
27
+ occupancyGroup: string;
28
+ /** The finish-location / space type (e.g. `"corridor"`, `"exit_enclosure"`). */
29
+ spaceType: string;
30
+ /** Whether the space is sprinklered — relaxes several fire-finish floors. */
31
+ sprinklered: boolean;
32
+ }
33
+ /** The resolved jurisdiction the verdicts were evaluated against. */
34
+ export interface CodeAdvisoryJurisdiction {
35
+ id: string;
36
+ name: string;
37
+ codeFamily: string;
38
+ edition: string;
39
+ adoptionNote: string;
40
+ }
41
+ /** The echoed evaluation context (the space the badges apply to). */
42
+ export interface CodeAdvisoryContext {
43
+ occupancyGroup: string;
44
+ spaceType: string;
45
+ sprinklered: boolean;
46
+ }
47
+ /** The threshold one requirement imposes: a class floor and/or a numeric floor. */
48
+ export interface CodeRequirementThreshold {
49
+ /** The IBC classification scale this requirement is measured on (open id). */
50
+ scale: string;
51
+ /** The minimum acceptable class on `scale`, or null when only a numeric floor applies. */
52
+ minClass: string | null;
53
+ /** A numeric floor (e.g. a DCOF minimum), or null when only a class floor applies. */
54
+ numericFloor: {
55
+ metric: string;
56
+ value: number;
57
+ unit: string;
58
+ } | null;
59
+ }
60
+ /** The accepted fact a verdict was decided from (null when the material carried none). */
61
+ export interface CodeRequirementEvidence {
62
+ attributeId: OpenEnum<"fire_classifications" | "slip_resistance_dcof">;
63
+ value: string;
64
+ confidence: number | null;
65
+ sourceBasis: string | null;
66
+ }
67
+ /** One requirement badged against one material. */
68
+ export interface CodeRequirementVerdict {
69
+ requirementId: string;
70
+ dimension: OpenEnum<"fire" | "floor" | "slip" | "decorative">;
71
+ section: string;
72
+ requirement: CodeRequirementThreshold;
73
+ /** `meets` / `shortfall` / `unknown` — `unknown` when no defensible fact maps onto `scale`. */
74
+ verdict: OpenEnum<"meets" | "shortfall" | "unknown">;
75
+ evidence: CodeRequirementEvidence | null;
76
+ /** The AHJ advisory caveat — model code as adopted, never a legal determination. */
77
+ caveat: string;
78
+ }
79
+ /** Per-variant advisory outcome, in input order. */
80
+ export interface CodeAdvisoryVariantResult {
81
+ variantId: string;
82
+ /** `evaluated` when the id resolved; `unknown_variant` (with `requirements: []`) when it did not. */
83
+ state: OpenEnum<"evaluated" | "unknown_variant">;
84
+ requirements: CodeRequirementVerdict[];
85
+ }
86
+ /** The `POST /api/v1/codes/advisory` envelope — per-variant, per-requirement verdicts. */
87
+ export interface CodeAdvisoryResponse {
88
+ jurisdiction: CodeAdvisoryJurisdiction;
89
+ context: CodeAdvisoryContext;
90
+ /** The AHJ advisory caveat, at the envelope level too. */
91
+ caveat: string;
92
+ results: CodeAdvisoryVariantResult[];
93
+ }
package/codes-types.js ADDED
@@ -0,0 +1 @@
1
+ export {};