@instruments/materialgraph 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/by-spec-types.d.ts +73 -0
  2. package/by-spec-types.js +7 -0
  3. package/client.d.ts +186 -7
  4. package/client.js +242 -125
  5. package/codes-client.d.ts +82 -0
  6. package/codes-client.js +73 -0
  7. package/codes-types.d.ts +238 -0
  8. package/codes-types.js +1 -0
  9. package/colour-aggregate-types.d.ts +93 -0
  10. package/colour-aggregate-types.js +1 -0
  11. package/colours-client.d.ts +52 -0
  12. package/colours-client.js +39 -0
  13. package/composition-types.d.ts +343 -0
  14. package/composition-types.js +10 -0
  15. package/http-transport.d.ts +74 -0
  16. package/http-transport.js +201 -0
  17. package/index.d.ts +19 -0
  18. package/index.js +9 -0
  19. package/match-stream-types.d.ts +150 -0
  20. package/match-stream-types.js +28 -0
  21. package/match-stream.d.ts +22 -0
  22. package/match-stream.js +280 -0
  23. package/material-block-types.d.ts +23 -2
  24. package/material-types.d.ts +92 -0
  25. package/material-types.js +1 -0
  26. package/ndjson.d.ts +46 -0
  27. package/ndjson.js +103 -0
  28. package/operations.d.ts +6 -2
  29. package/operations.js +30 -2
  30. package/package.json +1 -1
  31. package/project-types.d.ts +67 -0
  32. package/project-types.js +16 -0
  33. package/projects.d.ts +13 -0
  34. package/projects.js +46 -0
  35. package/run-events.d.ts +82 -0
  36. package/run-events.js +29 -0
  37. package/run-types.d.ts +266 -0
  38. package/run-types.js +6 -0
  39. package/runs.d.ts +84 -0
  40. package/runs.js +253 -0
  41. package/schedule-types.d.ts +88 -0
  42. package/schedule-types.js +12 -0
  43. package/search-refinement-types.d.ts +26 -1
  44. package/search-result-validation.d.ts +27 -0
  45. package/search-result-validation.js +174 -0
  46. package/source-material-validation.d.ts +3 -0
  47. package/source-material-validation.js +109 -0
  48. package/spec-types.d.ts +82 -0
  49. package/spec-types.js +44 -0
  50. package/specifications.d.ts +72 -0
  51. package/specifications.js +82 -0
  52. package/sse.d.ts +35 -0
  53. package/sse.js +147 -0
  54. package/types.d.ts +204 -77
  55. package/validation.d.ts +119 -0
  56. package/validation.js +241 -0
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Hand-mirrored wire types for the synchronous composition-variation surface
3
+ * (`POST /api/v1/schemes/vary`, operation `vary_schemes`, MG-438). Like the rest
4
+ * of the SDK these interfaces carry NO runtime dependency on any
5
+ * `@materialgraph/*` package — they mirror the `VarySchemesRequest` /
6
+ * `VarySchemesResponse` shapes in `apps/web/public/openapi.yaml`.
7
+ *
8
+ * Vary is deterministic and synchronous: it re-derives an "option B" from a
9
+ * succeeded `compose-material-schemes` Run along one constraint axis, so unlike
10
+ * the async Run surface the response IS the scheme — no run envelope, no events.
11
+ *
12
+ * TOLERANT-PARSING CONTRACT: response enums are widened with {@link OpenEnum}
13
+ * so a new server value still type-checks; objects are read structurally.
14
+ */
15
+ import type { OpenEnum } from "./types.js";
16
+ /**
17
+ * The four v1 variation axes. Request-side, so this is a STRICT union (the
18
+ * caller picks exactly one). `uniqueness` is deferred server-side (MG-445).
19
+ */
20
+ export type VaryAxis = "palette" | "sustainability" | "brandDiversity" | "priceTier";
21
+ /**
22
+ * The variation axes as a runtime array, for consumers that render an axis
23
+ * picker. Order is the canonical presentation order.
24
+ */
25
+ export declare const VARY_AXES: readonly VaryAxis[];
26
+ /** Pin one slot to a specific variant so vary re-derives only the rest. */
27
+ export interface SchemeLock {
28
+ slotId: string;
29
+ /** Must equal that slot's CURRENT pick in the source scheme. */
30
+ variantId: string;
31
+ }
32
+ /** Candidate exclusions applied on top of the automatic source-pick exclusion. */
33
+ export interface SchemeExclusions {
34
+ variantIds?: string[];
35
+ brandSlugs?: string[];
36
+ }
37
+ /** Body for `POST /api/v1/schemes/vary` ({@link MaterialGraphClient.varyScheme}). */
38
+ export interface VarySchemesRequest {
39
+ /** The succeeded `compose-material-schemes` Run to vary from. */
40
+ runId: string;
41
+ /** Which scheme in the Run is option A. Defaults to the Run's FIRST scheme. */
42
+ schemeId?: string;
43
+ axis: VaryAxis;
44
+ /** Slots kept pinned to their source pick; only unlocked slots re-derive. */
45
+ locks?: SchemeLock[];
46
+ exclusions?: SchemeExclusions;
47
+ }
48
+ /**
49
+ * One anchor (MG-613): a variant the designer has already pinned to a place in
50
+ * the programme, supplied on the `compose-material-schemes` run input's
51
+ * `anchors[]`. Exactly ONE of `variantId` (an exact catalog variant) or `query`
52
+ * (free-text "Brand Product Colour", resolved server-side) is required. An
53
+ * anchored slot is LOCKED — composition completes the OPEN slots in relation to
54
+ * it. Anchors that cannot be resolved leave the slot open and surface a run
55
+ * warning (never a failure).
56
+ */
57
+ export interface ComposeAnchorInput {
58
+ /** The building element the anchor fills (flooring, wall, textile, …). */
59
+ element: SlotElement;
60
+ /** Optional named space to scope the anchor to (matched by name/slug). */
61
+ spaceName?: string;
62
+ /** An exact catalog variant id. Mutually exclusive with `query`. */
63
+ variantId?: string;
64
+ /** Free-text "Brand Product Colour". Mutually exclusive with `variantId`. */
65
+ query?: string;
66
+ /** Display name for slot naming and warnings. */
67
+ label?: string;
68
+ }
69
+ /**
70
+ * One standing rule in a project's compliance preset (MG-615): free-text
71
+ * `criteria` phrases that apply to a `scope` of slots. `scope: "all"` reaches
72
+ * every slot (including slots with no element and slots classified `other`); a
73
+ * named element reaches only slots carrying exactly that element, so an
74
+ * unclassified slot receives `"all"` rules only. Phrases are resolved
75
+ * server-side through the same vocabulary as brief criteria — sustainability
76
+ * wording ("LEED", "GREENGUARD Gold") resolves to a named certification
77
+ * programme. A phrase that grounds to nothing is dropped with a run warning.
78
+ */
79
+ export interface CompliancePresetRule {
80
+ scope: "all" | SlotElement;
81
+ criteria: string[];
82
+ }
83
+ /** A project's standing rules, declared once on the compose run (max 12 rules). */
84
+ export interface CompliancePreset {
85
+ rules: CompliancePresetRule[];
86
+ }
87
+ /**
88
+ * The `inputJson` for a `compose-material-schemes` run. `batchId` is required;
89
+ * everything else has a server default. `anchors` (max 24) carry externally
90
+ * decided picks (MG-613); `compliancePreset` carries the project's standing
91
+ * rules (MG-615), which cascade into slots as PREFERRED criteria — they grade
92
+ * every candidate but never exclude one.
93
+ */
94
+ export interface ComposeMaterialSchemesInput {
95
+ batchId: string;
96
+ count?: number;
97
+ seed?: number;
98
+ exclusions?: SchemeExclusions;
99
+ anchors?: ComposeAnchorInput[];
100
+ compliancePreset?: CompliancePreset;
101
+ }
102
+ /**
103
+ * The state of one criterion verdict cell (MG-627: the shared MaterialGraph
104
+ * verdict states). The six LIVE states are `matched` (accepted value satisfies
105
+ * the criterion), `deviated` (directionally off but named), `failed` (an
106
+ * accepted value exists and does not satisfy it), `unverified` (a real value
107
+ * that did not clear the trust bar), `missing` (no accepted value to evaluate),
108
+ * and `not_evaluated` (the engine did not check it). `mismatched`/`unknown` are
109
+ * the two LEGACY tokens old persisted runs still carry (never emitted again).
110
+ * Open per the response-enum policy: treat unknown states forward-compatibly.
111
+ */
112
+ export type CompositionMatchState = OpenEnum<"matched" | "deviated" | "failed" | "unverified" | "missing" | "not_evaluated" | "mismatched" | "unknown">;
113
+ /** How a criterion cell was baselined — what the candidate was compared against. */
114
+ export type CompositionMatchBaseline = {
115
+ kind: "term";
116
+ term: string;
117
+ } | {
118
+ kind: "presence";
119
+ } | {
120
+ kind: "seed";
121
+ variantId: string;
122
+ };
123
+ /** One resolved criterion → material verdict cell, as graded during composition. */
124
+ export interface CompositionMatch {
125
+ attributeId: string;
126
+ state: CompositionMatchState;
127
+ confidence: number | null;
128
+ sourceBasis: string | null;
129
+ /** Required vs preferred criterion (MG-627). Absent on legacy persisted runs. */
130
+ strength?: "required" | "preferred";
131
+ /** What the cell was evaluated against (MG-627). Absent on legacy persisted runs. */
132
+ baseline?: CompositionMatchBaseline;
133
+ /** The candidate's own accepted value label, when the cell carries one (MG-627). */
134
+ value?: string | null;
135
+ /** Why an `unverified` cell missed the trust bar (MG-627), else null. */
136
+ trustGap?: "below_confidence" | "basis_unmet" | "proposed" | null;
137
+ }
138
+ /** One slot's chosen material plus the reason it was picked. */
139
+ export interface CompositionPick {
140
+ slotId: string;
141
+ slotName: string;
142
+ spaceId?: string | null;
143
+ element?: string | null;
144
+ material: {
145
+ variantId: string;
146
+ brand: string;
147
+ product: string;
148
+ variant: string;
149
+ imageUrl: string | null;
150
+ hex: string | null;
151
+ };
152
+ reason: {
153
+ score: number;
154
+ matched: CompositionMatch[];
155
+ notes: string[];
156
+ };
157
+ /**
158
+ * True when this pick is an ANCHOR the designer pinned to the slot (MG-613):
159
+ * composition never replaced it and vary keeps it fixed. Absent for ordinary
160
+ * composed picks.
161
+ */
162
+ locked?: boolean;
163
+ }
164
+ /** The multi-objective score for one scheme (each component in [0, 1]). */
165
+ export interface CompositionObjective {
166
+ total: number;
167
+ briefFit: number;
168
+ coverage: number;
169
+ coherence: number;
170
+ novelty: number;
171
+ }
172
+ /** A hydrated scheme: one pick per slot, plus its objective breakdown. */
173
+ export interface CompositionScheme {
174
+ schemeId: string;
175
+ title: string;
176
+ picks: CompositionPick[];
177
+ objective: CompositionObjective;
178
+ }
179
+ /** How far short of the request the derivation fell, with honest reasons. */
180
+ export interface CompositionShortfall {
181
+ requested: number;
182
+ produced: number;
183
+ reasons: string[];
184
+ }
185
+ /** The axis + source provenance stamped onto every varied scheme. */
186
+ export interface VaryProvenance {
187
+ axis: VaryAxis;
188
+ sourceRunId: string;
189
+ sourceSchemeId: string;
190
+ lockedSlotIds: string[];
191
+ /**
192
+ * Human-readable notes recording exactly what the axis did — counts, not
193
+ * adjectives (e.g. "brandDiversity: hard-excluded 2 brand(s) appearing ≥2×").
194
+ * These are the variation's receipts.
195
+ */
196
+ notes: string[];
197
+ }
198
+ /** The `200` body from `POST /api/v1/schemes/vary`. */
199
+ export interface VarySchemesResponse {
200
+ /** The composition version the scheme was stamped with. */
201
+ version: string;
202
+ scheme: CompositionScheme;
203
+ provenance: VaryProvenance;
204
+ shortfall: CompositionShortfall | null;
205
+ }
206
+ /**
207
+ * Whether a programme element (space, slot, or palette) was `stated` in a text
208
+ * source or `inferred` from imagery/mood. Only inferred elements raise a review
209
+ * question. Absent → treated as `stated`.
210
+ */
211
+ export type ProgrammeProvenance = "stated" | "inferred";
212
+ /**
213
+ * The broad building element a slot fills — a pragmatically-extended Uniclass EF
214
+ * vocabulary. Widened with {@link OpenEnum} so a newer server term still
215
+ * type-checks.
216
+ */
217
+ export type SlotElement = OpenEnum<"flooring" | "wall" | "ceiling" | "textile" | "surface" | "furniture" | "lighting" | "other">;
218
+ /** One colour cue: a hex swatch with an optional human name. */
219
+ export interface BriefColourCue {
220
+ hex: string;
221
+ name?: string;
222
+ }
223
+ /** A source anchor (document id + optional page) attributing derived content. */
224
+ export interface CompositionSourceRef {
225
+ sourceId: string;
226
+ page: number | null;
227
+ }
228
+ /**
229
+ * One candidate colour direction the pile's colour evidence implies when no
230
+ * palette was explicitly committed. `directionId` is a stable kebab-case handle;
231
+ * `sourceRefs` attribute the direction to the palette source(s) it was drawn
232
+ * from (empty when unattributable).
233
+ */
234
+ export interface PaletteDirection {
235
+ directionId: string;
236
+ name: string;
237
+ colours: BriefColourCue[];
238
+ sourceRefs: CompositionSourceRef[];
239
+ }
240
+ /**
241
+ * Suspend payload for the compose `review-programme` step: the FULL programme
242
+ * picture — every space and slot tagged with its provenance — so the card can
243
+ * show the whole thing and highlight what was inferred.
244
+ */
245
+ export interface ReviewProgrammeSuspendPayload {
246
+ spaces: Array<{
247
+ spaceId: string;
248
+ name: string;
249
+ spaceType?: string;
250
+ provenance: ProgrammeProvenance;
251
+ }>;
252
+ slots: Array<{
253
+ slotId: string;
254
+ name: string;
255
+ spaceId?: string;
256
+ element?: SlotElement;
257
+ provenance: ProgrammeProvenance;
258
+ }>;
259
+ }
260
+ /**
261
+ * Resume grammar for the `review-programme` step: drop a slot, drop a space
262
+ * (cascade-dropping its slots), or rename a space.
263
+ */
264
+ export interface ReviewProgrammeResumeData {
265
+ corrections: Array<{
266
+ action: "drop-slot";
267
+ slotId: string;
268
+ } | {
269
+ action: "drop-space";
270
+ spaceId: string;
271
+ } | {
272
+ action: "rename-space";
273
+ spaceId: string;
274
+ name: string;
275
+ }>;
276
+ }
277
+ /**
278
+ * Suspend payload for the compose `commit-palette` step. Today only the
279
+ * `directions` mode ships; the shape carries `mode` so a later `ab-images` mode
280
+ * is an additive member, not a migration.
281
+ */
282
+ export interface CommitPaletteSuspendPayload {
283
+ mode: OpenEnum<"directions">;
284
+ directions: PaletteDirection[];
285
+ }
286
+ /**
287
+ * Resume grammar for the `commit-palette` step: commit to one direction (by id)
288
+ * or adopt the union of every direction's colours.
289
+ */
290
+ export type CommitPaletteResumeData = {
291
+ choice: "direction";
292
+ directionId: string;
293
+ } | {
294
+ choice: "all";
295
+ };
296
+ /**
297
+ * Resume grammar for the `review-material-criteria` step: accept a resolved
298
+ * attribute/term for an unresolved phrase, or drop it.
299
+ */
300
+ export interface ReviewCriteriaResumeData {
301
+ corrections: Array<{
302
+ action: "accept";
303
+ slotId: string;
304
+ raw: string;
305
+ attributeId: string;
306
+ term?: string;
307
+ } | {
308
+ action: "drop";
309
+ slotId: string;
310
+ raw: string;
311
+ }>;
312
+ }
313
+ /** The union of every socratic-step resume payload, keyed by the suspended step. */
314
+ export type InterviewResumeData = ReviewCriteriaResumeData | ReviewProgrammeResumeData | CommitPaletteResumeData;
315
+ /** The three socratic interview steps, in procedure order. */
316
+ export type InterviewStepId = "review-programme" | "commit-palette" | "review-material-criteria";
317
+ /**
318
+ * Payload of a `composition.answered` event — the pile answered a question with
319
+ * no human turn, or a suspension resolved. `answer` is a stable slug; `citation`
320
+ * attributes an answer to its source when the brief stated it.
321
+ */
322
+ export interface CompositionAnsweredPayload {
323
+ step: InterviewStepId;
324
+ answer: string;
325
+ citation: CompositionSourceRef | null;
326
+ counts?: Record<string, number>;
327
+ }
328
+ /**
329
+ * Payload of a `composition.slot-pool` event — the candidate theater for one
330
+ * slot: how many were considered and the top picks (≤5) hydrated for identity.
331
+ */
332
+ export interface SlotPoolEventPayload {
333
+ slotId: string;
334
+ slotName: string;
335
+ considered: number;
336
+ top: Array<{
337
+ variantId: string;
338
+ brand: string;
339
+ product: string;
340
+ imageUrl: string | null;
341
+ score: number;
342
+ }>;
343
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The variation axes as a runtime array, for consumers that render an axis
3
+ * picker. Order is the canonical presentation order.
4
+ */
5
+ export const VARY_AXES = Object.freeze([
6
+ "palette",
7
+ "sustainability",
8
+ "brandDiversity",
9
+ "priceTier",
10
+ ]);
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The shared low-level HTTP transport for {@link MaterialGraphClient}. It owns
3
+ * URL building, the bearer header, rate-limit capture, response reading, and the
4
+ * one error-mapping path — so the JSON read surface (`client.ts`), the multipart
5
+ * upload, the SSE stream (`runs.ts`), and the NDJSON Match stream
6
+ * (`match-stream.ts`) all fail identically. Retry-free by design; a single
7
+ * failure surfaces as one {@link MaterialGraphError}.
8
+ */
9
+ import { type RateLimit } from "./errors.js";
10
+ import { type SdkOperation } from "./operations.js";
11
+ export type QueryValue = string | number | boolean | undefined | null | Array<string | number | boolean>;
12
+ export type QueryParams = Record<string, QueryValue>;
13
+ export interface RequestOptions {
14
+ query?: QueryParams;
15
+ body?: unknown;
16
+ signal?: AbortSignal;
17
+ pathParams?: Readonly<Record<string, string>>;
18
+ }
19
+ export interface StreamRequestOptions extends RequestOptions {
20
+ /** The streaming media type to negotiate (`text/event-stream`, `application/x-ndjson`). */
21
+ accept: string;
22
+ /** Names this stream in the "returned no body" diagnostic, e.g. `"match stream"`. */
23
+ subject: string;
24
+ }
25
+ /** A read, status-checked HTTP response with its parsed body. */
26
+ export interface SentResponse {
27
+ response: Response;
28
+ text: string;
29
+ parsed: unknown;
30
+ }
31
+ interface SendInput {
32
+ method: string;
33
+ url: string;
34
+ headers: Record<string, string>;
35
+ /** The only bodies the SDK sends: a JSON string or a multipart `FormData`. */
36
+ body?: string | FormData;
37
+ signal?: AbortSignal;
38
+ }
39
+ export interface HttpTransportOptions {
40
+ apiKey: string;
41
+ baseUrl: string;
42
+ fetch: typeof fetch;
43
+ }
44
+ export declare class HttpTransport {
45
+ #private;
46
+ readonly baseUrl: string;
47
+ readonly apiKey: string;
48
+ readonly fetch: typeof fetch;
49
+ constructor(options: HttpTransportOptions);
50
+ get lastRateLimit(): RateLimit | null;
51
+ /** The bearer + JSON `Accept` headers, plus any per-request additions. */
52
+ authHeaders(extra?: Record<string, string>): Record<string, string>;
53
+ buildUrl(path: string, query?: QueryParams): string;
54
+ /**
55
+ * Send one request, capture rate limits, read the body, and map a non-2xx or
56
+ * transport failure to a {@link MaterialGraphError}. The parsed JSON body (or
57
+ * raw text when unparseable) is returned on success. Not for streaming reads —
58
+ * it consumes the whole body via `response.text()`.
59
+ */
60
+ send(input: SendInput): Promise<SentResponse>;
61
+ /**
62
+ * Open a streaming operation and hand back its UNREAD byte body — the one
63
+ * entry point for every long-lived read (SSE run events, NDJSON Match
64
+ * progress). Everything up to the first byte behaves exactly like
65
+ * {@link send}: same URL building, same bearer header, same rate-limit
66
+ * capture, same {@link MaterialGraphError} mapping for a transport failure or
67
+ * a non-2xx (whose short error body IS read, since it is not a stream).
68
+ * Framing and cancellation belong to the caller.
69
+ */
70
+ openStream(sdkOperation: SdkOperation, options: StreamRequestOptions): Promise<ReadableStream<Uint8Array>>;
71
+ /** Execute a registered JSON operation and return its typed body. */
72
+ requestJson<T>(sdkOperation: SdkOperation, options?: RequestOptions): Promise<T>;
73
+ }
74
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The shared low-level HTTP transport for {@link MaterialGraphClient}. It owns
3
+ * URL building, the bearer header, rate-limit capture, response reading, and the
4
+ * one error-mapping path — so the JSON read surface (`client.ts`), the multipart
5
+ * upload, the SSE stream (`runs.ts`), and the NDJSON Match stream
6
+ * (`match-stream.ts`) all fail identically. Retry-free by design; a single
7
+ * failure surfaces as one {@link MaterialGraphError}.
8
+ */
9
+ import { MaterialGraphError } from "./errors.js";
10
+ import { operationPath } from "./operations.js";
11
+ function parseIntOrNull(value) {
12
+ if (value === null)
13
+ return null;
14
+ const parsed = Number.parseInt(value, 10);
15
+ return Number.isNaN(parsed) ? null : parsed;
16
+ }
17
+ function parseRateLimit(headers) {
18
+ const limit = parseIntOrNull(headers.get("x-ratelimit-limit"));
19
+ const remaining = parseIntOrNull(headers.get("x-ratelimit-remaining"));
20
+ const reset = parseIntOrNull(headers.get("x-ratelimit-reset"));
21
+ const retryAfter = parseIntOrNull(headers.get("retry-after"));
22
+ if (limit === null && remaining === null && reset === null && retryAfter === null) {
23
+ return null;
24
+ }
25
+ return { limit, remaining, reset, retryAfter };
26
+ }
27
+ export class HttpTransport {
28
+ baseUrl;
29
+ apiKey;
30
+ fetch;
31
+ #lastRateLimit = null;
32
+ constructor(options) {
33
+ this.apiKey = options.apiKey;
34
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
35
+ this.fetch = options.fetch;
36
+ }
37
+ get lastRateLimit() {
38
+ return this.#lastRateLimit;
39
+ }
40
+ /** The bearer + JSON `Accept` headers, plus any per-request additions. */
41
+ authHeaders(extra) {
42
+ return { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json", ...extra };
43
+ }
44
+ buildUrl(path, query) {
45
+ const url = new URL(`${this.baseUrl}${path}`);
46
+ if (query) {
47
+ for (const [key, value] of Object.entries(query)) {
48
+ if (value === undefined || value === null)
49
+ continue;
50
+ if (Array.isArray(value)) {
51
+ for (const item of value) {
52
+ if (item !== undefined && item !== null)
53
+ url.searchParams.append(key, String(item));
54
+ }
55
+ }
56
+ else {
57
+ url.searchParams.append(key, String(value));
58
+ }
59
+ }
60
+ }
61
+ return url.toString();
62
+ }
63
+ /**
64
+ * Send one request, capture rate limits, read the body, and map a non-2xx or
65
+ * transport failure to a {@link MaterialGraphError}. The parsed JSON body (or
66
+ * raw text when unparseable) is returned on success. Not for streaming reads —
67
+ * it consumes the whole body via `response.text()`.
68
+ */
69
+ async send(input) {
70
+ const requestUrl = new URL(input.url);
71
+ const metadata = {
72
+ method: input.method,
73
+ origin: requestUrl.origin,
74
+ path: requestUrl.pathname,
75
+ };
76
+ let response;
77
+ try {
78
+ response = await this.fetch(input.url, {
79
+ method: input.method,
80
+ headers: input.headers,
81
+ body: input.body,
82
+ signal: input.signal,
83
+ });
84
+ }
85
+ catch (cause) {
86
+ throw MaterialGraphError.fromTransport({ ...metadata, cause, callerSignal: input.signal });
87
+ }
88
+ const rateLimit = parseRateLimit(response.headers);
89
+ this.#lastRateLimit = rateLimit;
90
+ let text;
91
+ try {
92
+ text = await response.text();
93
+ }
94
+ catch (cause) {
95
+ throw MaterialGraphError.fromTransport({ ...metadata, cause, callerSignal: input.signal });
96
+ }
97
+ let parsed;
98
+ if (text.length > 0) {
99
+ try {
100
+ parsed = JSON.parse(text);
101
+ }
102
+ catch {
103
+ parsed = text;
104
+ }
105
+ }
106
+ if (!response.ok) {
107
+ throw MaterialGraphError.fromResponse({
108
+ ...metadata,
109
+ status: response.status,
110
+ body: parsed,
111
+ responseText: text,
112
+ rateLimit,
113
+ apiKey: this.apiKey,
114
+ queryValues: [...requestUrl.searchParams.values()],
115
+ });
116
+ }
117
+ return { response, text, parsed };
118
+ }
119
+ /**
120
+ * Open a streaming operation and hand back its UNREAD byte body — the one
121
+ * entry point for every long-lived read (SSE run events, NDJSON Match
122
+ * progress). Everything up to the first byte behaves exactly like
123
+ * {@link send}: same URL building, same bearer header, same rate-limit
124
+ * capture, same {@link MaterialGraphError} mapping for a transport failure or
125
+ * a non-2xx (whose short error body IS read, since it is not a stream).
126
+ * Framing and cancellation belong to the caller.
127
+ */
128
+ async openStream(sdkOperation, options) {
129
+ const url = this.buildUrl(operationPath(sdkOperation, options.pathParams), options.query);
130
+ const requestUrl = new URL(url);
131
+ const metadata = {
132
+ method: sdkOperation.method,
133
+ origin: requestUrl.origin,
134
+ path: requestUrl.pathname,
135
+ };
136
+ const hasBody = options.body !== undefined;
137
+ let response;
138
+ try {
139
+ response = await this.fetch(url, {
140
+ method: sdkOperation.method,
141
+ headers: this.authHeaders({
142
+ Accept: options.accept,
143
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
144
+ }),
145
+ body: hasBody ? JSON.stringify(options.body) : undefined,
146
+ signal: options.signal,
147
+ });
148
+ }
149
+ catch (cause) {
150
+ throw MaterialGraphError.fromTransport({ ...metadata, cause, callerSignal: options.signal });
151
+ }
152
+ const rateLimit = parseRateLimit(response.headers);
153
+ this.#lastRateLimit = rateLimit;
154
+ if (!response.ok) {
155
+ let text = "";
156
+ try {
157
+ text = await response.text();
158
+ }
159
+ catch {
160
+ // The status IS the error; a body that will not read adds nothing and
161
+ // must not mask it. Report the failure with an empty detail instead.
162
+ text = "";
163
+ }
164
+ let parsed;
165
+ if (text.length > 0) {
166
+ try {
167
+ parsed = JSON.parse(text);
168
+ }
169
+ catch {
170
+ parsed = text;
171
+ }
172
+ }
173
+ throw MaterialGraphError.fromResponse({
174
+ ...metadata,
175
+ status: response.status,
176
+ body: parsed,
177
+ responseText: text,
178
+ rateLimit,
179
+ apiKey: this.apiKey,
180
+ queryValues: [...requestUrl.searchParams.values()],
181
+ });
182
+ }
183
+ if (response.body === null) {
184
+ throw MaterialGraphError.unexpected(`MaterialGraph ${options.subject} returned no body`);
185
+ }
186
+ return response.body;
187
+ }
188
+ /** Execute a registered JSON operation and return its typed body. */
189
+ async requestJson(sdkOperation, options = {}) {
190
+ const url = this.buildUrl(operationPath(sdkOperation, options.pathParams), options.query);
191
+ const hasBody = options.body !== undefined;
192
+ const { parsed } = await this.send({
193
+ method: sdkOperation.method,
194
+ url,
195
+ headers: this.authHeaders(hasBody ? { "Content-Type": "application/json" } : undefined),
196
+ body: hasBody ? JSON.stringify(options.body) : undefined,
197
+ signal: options.signal,
198
+ });
199
+ return parsed;
200
+ }
201
+ }
package/index.d.ts CHANGED
@@ -2,11 +2,30 @@ export { MaterialGraphClient, DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client.j
2
2
  export type { MaterialGraphClientOptions, BrowseNamespace } from "./client.js";
3
3
  export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS } from "./operations.js";
4
4
  export type { SdkOperation, SdkOperationId } from "./operations.js";
5
+ export { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
6
+ export type * from "./run-types.js";
7
+ export type * from "./schedule-types.js";
8
+ export { VARY_AXES } from "./composition-types.js";
9
+ export type * from "./composition-types.js";
10
+ export { PROJECT_EVENT_KINDS } from "./project-types.js";
11
+ export type * from "./project-types.js";
12
+ export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
13
+ export type * from "./run-events.js";
5
14
  export { MaterialGraphError } from "./errors.js";
6
15
  export type { MaterialGraphErrorInit, MaterialGraphErrorKind, MaterialGraphRequestMetadata, ProblemDetail, RateLimit, SanitizedErrorValue, SanitizedIssue, SimpleErrorBody, } from "./errors.js";
7
16
  export type * from "./types.js";
17
+ export { MATCH_IMAGE_TYPES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
18
+ export type * from "./match-stream-types.js";
19
+ export { CodesClient } from "./codes-client.js";
20
+ export type * from "./codes-types.js";
21
+ export { ColoursClient } from "./colours-client.js";
22
+ export type { ColoursTransport } from "./colours-client.js";
23
+ export type * from "./colour-aggregate-types.js";
8
24
  export type * from "./brand-types.js";
9
25
  export type * from "./material-asset-types.js";
10
26
  export type * from "./material-block-types.js";
11
27
  export type * from "./schema-resource-types.js";
12
28
  export type * from "./search-refinement-types.js";
29
+ export { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID } from "./spec-types.js";
30
+ export type * from "./spec-types.js";
31
+ export { certificationProgramme, certificationProgrammeCriterion, hasSpec, specCriterion, specTerm, } from "./specifications.js";
package/index.js CHANGED
@@ -1,3 +1,12 @@
1
1
  export { MaterialGraphClient, DEFAULT_BASE_URL, MAX_BATCH_IDS } from "./client.js";
2
2
  export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS } from "./operations.js";
3
+ export { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
4
+ export { VARY_AXES } from "./composition-types.js";
5
+ export { PROJECT_EVENT_KINDS } from "./project-types.js";
6
+ export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
3
7
  export { MaterialGraphError } from "./errors.js";
8
+ export { MATCH_IMAGE_TYPES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
9
+ export { CodesClient } from "./codes-client.js";
10
+ export { ColoursClient } from "./colours-client.js";
11
+ export { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID } from "./spec-types.js";
12
+ export { certificationProgramme, certificationProgrammeCriterion, hasSpec, specCriterion, specTerm, } from "./specifications.js";