@instruments/materialgraph 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/association-types.d.ts +38 -27
  2. package/associations-client.d.ts +4 -4
  3. package/associations-client.js +3 -10
  4. package/attribution-types.d.ts +4 -4
  5. package/brand-types.d.ts +14 -6
  6. package/brands-client.d.ts +7 -38
  7. package/brands-client.js +10 -40
  8. package/by-colour-types.d.ts +58 -31
  9. package/by-spec-types.d.ts +6 -73
  10. package/by-spec-types.js +7 -5
  11. package/catalog-discovery-types.d.ts +11 -26
  12. package/catalogue-client.d.ts +16 -35
  13. package/catalogue-client.js +32 -67
  14. package/catalogue-types.d.ts +44 -4
  15. package/categories-client.d.ts +63 -0
  16. package/categories-client.js +48 -0
  17. package/category-types.d.ts +114 -0
  18. package/citation-types.d.ts +4 -4
  19. package/client-config.d.ts +4 -6
  20. package/client-config.js +2 -4
  21. package/client-source-material-comments.d.ts +4 -4
  22. package/client-source-material-comments.js +4 -38
  23. package/client-vocabulary.d.ts +7 -9
  24. package/client-vocabulary.js +22 -30
  25. package/client.d.ts +110 -244
  26. package/client.js +217 -387
  27. package/codes-client.d.ts +2 -2
  28. package/codes-client.js +8 -8
  29. package/codes-types.d.ts +55 -34
  30. package/colour-aggregate-types.d.ts +24 -24
  31. package/colours-client.d.ts +6 -11
  32. package/colours-client.js +11 -19
  33. package/coordinate-client.d.ts +2 -2
  34. package/coordinate-client.js +1 -13
  35. package/corpus-scope-types.d.ts +3 -3
  36. package/corpus-scope-types.js +1 -25
  37. package/demand-batch-types.d.ts +18 -18
  38. package/demand-types.d.ts +24 -24
  39. package/errors.d.ts +13 -13
  40. package/errors.js +54 -52
  41. package/http-transport.d.ts +13 -21
  42. package/http-transport.js +31 -28
  43. package/identify-types.d.ts +54 -44
  44. package/identify-types.js +5 -3
  45. package/index.d.ts +11 -27
  46. package/index.js +4 -6
  47. package/list-variant-facets-types.d.ts +60 -0
  48. package/list-variants-types.d.ts +142 -0
  49. package/list-variants-types.js +12 -0
  50. package/match-stream-types.d.ts +45 -32
  51. package/match-stream-types.js +2 -0
  52. package/match-stream.d.ts +2 -2
  53. package/match-stream.js +67 -49
  54. package/match-types.d.ts +32 -8
  55. package/material-asset-types.d.ts +89 -69
  56. package/material-block-types.d.ts +8 -6
  57. package/material-composition-client.d.ts +21 -0
  58. package/material-composition-client.js +13 -0
  59. package/material-composition-types.d.ts +403 -0
  60. package/material-query-plan-types.d.ts +61 -145
  61. package/material-resources-client.d.ts +12 -10
  62. package/material-resources-client.js +27 -22
  63. package/material-types.d.ts +16 -24
  64. package/ndjson.js +16 -8
  65. package/operations.d.ts +6 -6
  66. package/operations.js +29 -156
  67. package/package.json +5 -1
  68. package/palette-search-types.d.ts +87 -52
  69. package/query-understanding-types.d.ts +65 -40
  70. package/resolve-client.d.ts +8 -12
  71. package/resolve-client.js +9 -13
  72. package/resolve-colour-types.d.ts +8 -8
  73. package/resolve-types.d.ts +10 -10
  74. package/response-schemas.d.ts +20 -0
  75. package/response-schemas.generated.d.ts +5 -0
  76. package/response-schemas.generated.js +160 -0
  77. package/response-schemas.js +113 -0
  78. package/run-types.d.ts +64 -145
  79. package/runs.d.ts +9 -58
  80. package/runs.js +60 -149
  81. package/samplize-catalogue-types.d.ts +18 -18
  82. package/samplize-colorvec.d.ts +20 -20
  83. package/samplize-colorvec.js +66 -42
  84. package/schema-resource-types.d.ts +52 -39
  85. package/schema-resource-types.js +5 -5
  86. package/search-card-types.d.ts +26 -4
  87. package/search-index-plan-types.d.ts +70 -0
  88. package/search-refinement-types.d.ts +10 -40
  89. package/search-refinement-types.js +1 -0
  90. package/search-request-types.d.ts +121 -204
  91. package/search-response-receipt-types.d.ts +13 -253
  92. package/search-response-types.d.ts +2 -2
  93. package/search-result-validation.d.ts +3 -19
  94. package/search-result-validation.js +42 -183
  95. package/source-material-validation.d.ts +3 -3
  96. package/source-material-validation.js +35 -39
  97. package/spec-facet-types.d.ts +11 -16
  98. package/spec-types.d.ts +23 -16
  99. package/spec-types.js +8 -7
  100. package/specifications.d.ts +14 -72
  101. package/specifications.js +39 -89
  102. package/sse.d.ts +2 -2
  103. package/sse.js +32 -16
  104. package/substitute-types.d.ts +22 -22
  105. package/suggest-types.d.ts +11 -17
  106. package/suggest-types.js +3 -3
  107. package/taxonomy-coverage-types.d.ts +8 -8
  108. package/types.d.ts +202 -405
  109. package/validation.d.ts +26 -26
  110. package/validation.js +97 -92
  111. package/webmcp.d.ts +3 -3
  112. package/webmcp.generated.js +10 -93
  113. package/brand-most-sampled-types.d.ts +0 -33
  114. package/browse-types.d.ts +0 -236
  115. package/client-browse.d.ts +0 -78
  116. package/client-browse.js +0 -72
  117. package/client-exploration.d.ts +0 -9
  118. package/client-exploration.js +0 -7
  119. package/compare-types.d.ts +0 -89
  120. package/composition-client.d.ts +0 -33
  121. package/composition-client.js +0 -27
  122. package/composition-types.d.ts +0 -455
  123. package/composition-types.js +0 -10
  124. package/correction-types.d.ts +0 -88
  125. package/exploration-types.d.ts +0 -118
  126. package/exploration-types.js +0 -1
  127. package/fan-deck-types.d.ts +0 -42
  128. package/fan-deck-types.js +0 -9
  129. package/match-rejection-types.d.ts +0 -43
  130. package/match-rejection-types.js +0 -7
  131. package/palette-completion-types.d.ts +0 -115
  132. package/palette-completion-types.js +0 -1
  133. package/project-types.d.ts +0 -79
  134. package/project-types.js +0 -9
  135. package/projects.d.ts +0 -13
  136. package/projects.js +0 -46
  137. package/render-assets-types.d.ts +0 -65
  138. package/render-assets-types.js +0 -1
  139. package/run-events.d.ts +0 -106
  140. package/run-events.js +0 -32
  141. package/schedule-reference-types.d.ts +0 -46
  142. package/schedule-reference-types.js +0 -1
  143. package/schedule-references-client.d.ts +0 -14
  144. package/schedule-references-client.js +0 -26
  145. package/schedule-types.d.ts +0 -261
  146. package/schedule-types.js +0 -1
  147. package/search-collapse-types.d.ts +0 -69
  148. package/search-collapse-types.js +0 -1
  149. package/search-mood-types.d.ts +0 -6
  150. package/search-mood-types.js +0 -1
  151. package/search-window-types.d.ts +0 -25
  152. package/search-window-types.js +0 -1
  153. package/slot-pool-types.d.ts +0 -372
  154. package/slot-pool-types.js +0 -1
  155. /package/{brand-most-sampled-types.js → category-types.js} +0 -0
  156. /package/{browse-types.js → list-variant-facets-types.js} +0 -0
  157. /package/{compare-types.js → material-composition-types.js} +0 -0
  158. /package/{correction-types.js → search-index-plan-types.js} +0 -0
@@ -21,6 +21,9 @@ import type { SearchResultItem } from "./types.js";
21
21
  /** How the workflow classified the uploaded image. */
22
22
  export declare const MATCH_IMAGE_TYPES: readonly ["swatch", "installation", "flatplan"];
23
23
  export type MatchImageType = (typeof MATCH_IMAGE_TYPES)[number];
24
+ /** Why a Match run ended on an `error` frame (MG-730). */
25
+ export declare const MATCH_ERROR_REASONS: readonly ["upstream_unavailable", "failed"];
26
+ export type MatchErrorReason = (typeof MATCH_ERROR_REASONS)[number];
24
27
  /** What a detected surface is FOR — the element the material would serve. */
25
28
  export declare const MATCH_MATERIAL_FUNCTIONS: readonly ["flooring", "wall_surface", "ceiling", "partition", "upholstery", "millwork", "countertop", "accent", "sample"];
26
29
  export type MatchMaterialFunction = (typeof MATCH_MATERIAL_FUNCTIONS)[number];
@@ -42,14 +45,14 @@ export type MatchSurfaceStatus = (typeof MATCH_SURFACE_STATUSES)[number];
42
45
  export declare const MATCH_STREAM_EVENT_TYPES: readonly ["step", "classified", "surfaces", "surface", "heartbeat", "complete", "error"];
43
46
  export type MatchStreamEventType = (typeof MATCH_STREAM_EVENT_TYPES)[number];
44
47
  /** Where a surface sits in the image, in Gemini-style 0–1000 image space. */
45
- export interface MatchBoundingBox {
48
+ export type MatchBoundingBox = {
46
49
  x0: number;
47
50
  y0: number;
48
51
  x1: number;
49
52
  y1: number;
50
- }
53
+ };
51
54
  /** One surface the decompose step found in the image. */
52
- export interface MatchDetectedSurface {
55
+ export type MatchDetectedSurface = {
53
56
  id: string;
54
57
  label: string;
55
58
  materialFunction: MatchMaterialFunction;
@@ -61,7 +64,9 @@ export interface MatchDetectedSurface {
61
64
  boundingBox: MatchBoundingBox;
62
65
  /** JPEG data-URI crop used for Match retrieval — the thumbnail to show. */
63
66
  cropDataUri?: string;
64
- }
67
+ /** The `construction_methods` dictionary term the detector's finish names. */
68
+ construction?: string;
69
+ };
65
70
  /**
66
71
  * One surface's matching state and its ranked results. The same `surface.id`
67
72
  * arrives repeatedly as the run progresses (`matching` → `complete` / `error`);
@@ -86,7 +91,7 @@ export interface MatchDetectedSurface {
86
91
  * [0,1] — the same unit as each result's `confidence` — so they belong on one
87
92
  * axis. `poolSize` and `droppedBelowFloor` are counts and belong on neither.
88
93
  */
89
- export interface MatchComparison {
94
+ export type MatchComparison = {
90
95
  /** Candidates retrieved before the quality floor and product dedup. */
91
96
  poolSize: number;
92
97
  /** How many of those the quality floor rejected. */
@@ -98,8 +103,8 @@ export interface MatchComparison {
98
103
  * nothing was rejected, or when the rejects carried no readable signal.
99
104
  */
100
105
  bestRejected?: number;
101
- }
102
- export interface MatchSurfaceResult {
106
+ };
107
+ export type MatchSurfaceResult = {
103
108
  surface: MatchDetectedSurface;
104
109
  status: MatchSurfaceStatus;
105
110
  results: SearchResultItem[];
@@ -150,10 +155,10 @@ export interface MatchSurfaceResult {
150
155
  */
151
156
  colour?: MatchColourReading[];
152
157
  /** The segment's own extracted palette — what the colour stage matched against. */
153
- segmentPalette?: Array<{
158
+ segmentPalette?: {
154
159
  hex: string;
155
160
  proportion: number;
156
- }>;
161
+ }[];
157
162
  /**
158
163
  * One sentence of provenance a consumer may render verbatim, when the server
159
164
  * sent one — e.g. the `colour_fallback` explanation carrying the refused
@@ -162,13 +167,13 @@ export interface MatchSurfaceResult {
162
167
  note?: string;
163
168
  /** Present only on `status: "error"` — why this one surface failed. */
164
169
  error?: string;
165
- }
170
+ };
166
171
  /** The verifier's reading of one candidate, keyed back to its variant. */
167
- export interface MatchSenseCheckCell {
172
+ export type MatchSenseCheckCell = {
168
173
  variantId: string;
169
174
  verdict: MatchSenseCheckVerdict;
170
175
  reason: string;
171
- }
176
+ };
172
177
  export declare const MATCH_SENSE_CHECK_VERDICTS: readonly ["confirmed", "rejected", "abstained"];
173
178
  export type MatchSenseCheckVerdict = (typeof MATCH_SENSE_CHECK_VERDICTS)[number];
174
179
  /** The terminal Match workflow result — every surface in its final state. */
@@ -179,7 +184,7 @@ export type MatchSenseCheckVerdict = (typeof MATCH_SENSE_CHECK_VERDICTS)[number]
179
184
  */
180
185
  export declare const MATCH_OUTCOMES: readonly ["matched", "no_surfaces"];
181
186
  export type MatchOutcome = (typeof MATCH_OUTCOMES)[number];
182
- export interface MatchWorkflowResult {
187
+ export type MatchWorkflowResult = {
183
188
  /**
184
189
  * Result attribution (MG-1131) — `responseId` (the same value as the run's
185
190
  * `X-Request-Id` header), `basis: "match"`, and `issuedAt`. Every item in
@@ -214,10 +219,10 @@ export interface MatchWorkflowResult {
214
219
  */
215
220
  paletteMatches?: {
216
221
  results: SearchResultItem[];
217
- palette: Array<{
222
+ palette: {
218
223
  hex: string;
219
224
  proportion: number;
220
- }>;
225
+ }[];
221
226
  note: string;
222
227
  };
223
228
  /**
@@ -225,23 +230,23 @@ export interface MatchWorkflowResult {
225
230
  * nowhere else to put this. A scene run reports per surface instead.
226
231
  */
227
232
  comparison?: MatchComparison;
228
- }
233
+ };
229
234
  /** Progress narration for the phase the workflow just entered. */
230
- export interface MatchStreamStepEvent {
235
+ export type MatchStreamStepEvent = {
231
236
  type: "step";
232
237
  step: MatchWorkflowStep;
233
238
  message: string;
234
- }
239
+ };
235
240
  /** The image type the classify step settled on. */
236
- export interface MatchStreamClassifiedEvent {
241
+ export type MatchStreamClassifiedEvent = {
237
242
  type: "classified";
238
243
  imageType: MatchImageType;
239
- }
244
+ };
240
245
  /** Every surface the decompose step found, before any of them has matched. */
241
- export interface MatchStreamSurfacesEvent {
246
+ export type MatchStreamSurfacesEvent = {
242
247
  type: "surfaces";
243
248
  surfaces: MatchDetectedSurface[];
244
- }
249
+ };
245
250
  /**
246
251
  * The run's tally at the moment a `surface` event was emitted, so a consumer
247
252
  * reads the server's arithmetic rather than reimplementing it.
@@ -252,7 +257,7 @@ export interface MatchStreamSurfacesEvent {
252
257
  * `matchesSoFar`. `surfacesResolved === surfacesDetected` is therefore the
253
258
  * correct "the run is done" test, and never stalls at 3/4 on a failure.
254
259
  */
255
- export interface MatchRunTotals {
260
+ export type MatchRunTotals = {
256
261
  /** Surfaces the decompose step found. The denominator; fixed for the run. */
257
262
  surfacesDetected: number;
258
263
  /** Surfaces that reached a terminal state — `complete` OR `error`. */
@@ -261,14 +266,14 @@ export interface MatchRunTotals {
261
266
  surfacesFailed: number;
262
267
  /** Result items accumulated across every surface resolved so far. */
263
268
  matchesSoFar: number;
264
- }
269
+ };
265
270
  /** One surface's state changed — the frame that makes Match progressive. */
266
- export interface MatchStreamSurfaceEvent {
271
+ export type MatchStreamSurfaceEvent = {
267
272
  type: "surface";
268
273
  surface: MatchSurfaceResult;
269
274
  /** The run's tally as of this frame. Always present on a `surface` event. */
270
275
  totals: MatchRunTotals;
271
- }
276
+ };
272
277
  /**
273
278
  * CONNECTION LIVENESS ONLY — never progress. Proxies and load balancers kill
274
279
  * idle connections, so the server sends this when the stream has been quiet, and
@@ -276,18 +281,26 @@ export interface MatchStreamSurfaceEvent {
276
281
  * consumer should treat it as a state change: do not advance a step, reset a
277
282
  * no-progress watchdog, or count it towards anything.
278
283
  */
279
- export interface MatchStreamHeartbeatEvent {
284
+ export type MatchStreamHeartbeatEvent = {
280
285
  type: "heartbeat";
281
- }
286
+ };
282
287
  /** Terminal: the whole run finished and carries its assembled result. */
283
- export interface MatchStreamCompleteEvent {
288
+ export type MatchStreamCompleteEvent = {
284
289
  type: "complete";
285
290
  result: MatchWorkflowResult;
286
- }
291
+ };
287
292
  /** Terminal: the run failed as a whole (a single surface failing is not this). */
288
- export interface MatchStreamErrorEvent {
293
+ export type MatchStreamErrorEvent = {
289
294
  type: "error";
290
295
  message: string;
291
- }
296
+ /**
297
+ * `upstream_unavailable`: a provider (vision, embedding, vector index) could
298
+ * not answer, so nothing was looked at and a retry may succeed. `failed`: the
299
+ * run broke on something else. Absent from servers older than MG-730.
300
+ */
301
+ reason?: MatchErrorReason;
302
+ /** True only for `upstream_unavailable`. Absent from servers older than MG-730. */
303
+ retriable?: boolean;
304
+ };
292
305
  /** One frame of the Match progress stream, discriminated by `type`. */
293
306
  export type MatchStreamEvent = MatchStreamStepEvent | MatchStreamClassifiedEvent | MatchStreamSurfacesEvent | MatchStreamSurfaceEvent | MatchStreamHeartbeatEvent | MatchStreamCompleteEvent | MatchStreamErrorEvent;
@@ -4,6 +4,8 @@ export const MATCH_IMAGE_TYPES = [
4
4
  "installation",
5
5
  "flatplan",
6
6
  ];
7
+ /** Why a Match run ended on an `error` frame (MG-730). */
8
+ export const MATCH_ERROR_REASONS = ["upstream_unavailable", "failed"];
7
9
  /** What a detected surface is FOR — the element the material would serve. */
8
10
  export const MATCH_MATERIAL_FUNCTIONS = [
9
11
  "flooring",
package/match-stream.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { HttpTransport } from "./http-transport.js";
2
- import { type MatchStreamEvent } from "./match-stream-types.js";
2
+ import type { MatchStreamEvent } from "./match-stream-types.js";
3
3
  import type { MatchInput } from "./match-types.js";
4
4
  /**
5
5
  * Decode one NDJSON line into a validated {@link MatchStreamEvent}, raising a
@@ -7,7 +7,7 @@ import type { MatchInput } from "./match-types.js";
7
7
  * for `contract-parity.test.ts`, which re-checks it against the real Zod schema;
8
8
  * it is not part of the published surface.
9
9
  */
10
- export declare function decodeMatchStreamEvent(line: string): MatchStreamEvent;
10
+ export declare const decodeMatchStreamEvent: (line: string) => MatchStreamEvent;
11
11
  /**
12
12
  * Stream Match progress as an async iterator. Iterates until the run reaches a
13
13
  * `complete` or an `error` event, and raises if the connection ends before one
package/match-stream.js CHANGED
@@ -35,10 +35,9 @@
35
35
  * objects (`createMatchNdjsonStream`), so `parse` output is not what the wire
36
36
  * carries; reshaping to match it would invent a normalisation that never
37
37
  * happened.
38
- * 2. Stripping needs the COMPLETE key inventory of `searchResultItemSchema`,
39
- * whose closure reaches `candidateVerdictSchema` in `@materialgraph/contracts`
40
- * — a recursive hand-mirror that would have to stay in step forever, and a
41
- * wider surface than the validation it would serve.
38
+ * 2. Stripping needs the COMPLETE key inventory of `searchResultItemSchema`
39
+ * — a hand-mirror that would have to stay in step forever, and a wider
40
+ * surface than the validation it would serve.
42
41
  * 3. An additive server field then reaches consumers without an SDK release,
43
42
  * which matters while new EVENT types still require one (MG-658).
44
43
  * The ONE exception is `.default([])`: an omitted `results` / `inferredCategories`
@@ -50,14 +49,12 @@
50
49
  * optional string, `MatchCategoryInference` items, and every
51
50
  * `SearchResultItem` in `results[]` are checked exhaustively. Item validation is
52
51
  * the shared `checkSearchResultItem` (`search-result-validation.ts`), which
53
- * mirrors `searchResultItemSchema` and its whole closure down to
54
- * `candidateVerdictSchema` — including both of its `.superRefine()` cross-field
55
- * invariants.
52
+ * mirrors `searchResultItemSchema` and its whole closure.
56
53
  */
57
54
  import { MaterialGraphError } from "./errors.js";
58
- import { MATCH_IMAGE_TYPES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
55
+ import { MATCH_ERROR_REASONS, MATCH_IMAGE_TYPES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
59
56
  import { readNdjsonLines } from "./ndjson.js";
60
- import { findSdkOperation, } from "./operations.js";
57
+ import { findSdkOperation } from "./operations.js";
61
58
  import { checkSearchResultItems } from "./search-result-validation.js";
62
59
  import { requireArray, requireBoundedNumber, requireDefaultedArray, requireEnum, requireIntegerAtLeast, requireNumber, requireOptionalString, requireRecord, requireString, } from "./validation.js";
63
60
  /** The NDJSON media type the Match route negotiates the scene workflow on. */
@@ -70,34 +67,37 @@ const BOUNDING_BOX_CORNERS = Object.freeze([
70
67
  "x1",
71
68
  "y1",
72
69
  ]);
73
- function operation(operationId) {
70
+ const operation = (operationId) => {
74
71
  const found = findSdkOperation(operationId);
75
- if (!found)
72
+ if (!found) {
76
73
  throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
74
+ }
77
75
  return found;
78
- }
76
+ };
79
77
  /**
80
78
  * The failure shape every check in this module raises: one message naming the
81
79
  * offending path and quoting a bounded slice of the frame.
82
80
  */
83
- function frameFailure(line) {
81
+ const frameFailure = (line) => {
84
82
  const preview = line.length > MAX_FRAME_PREVIEW
85
83
  ? `${line.slice(0, MAX_FRAME_PREVIEW)}…`
86
84
  : line;
87
85
  return (reason) => MaterialGraphError.unexpected(`MaterialGraph match stream sent a malformed NDJSON frame: ${reason}. Frame: ${preview}`);
88
- }
86
+ };
89
87
  /** Check one `MatchCategoryInference` item. */
90
- function checkCategoryInference(value, what, fail) {
88
+ const checkCategoryInference = (value, what, fail) => {
91
89
  const record = requireRecord(value, what, fail);
92
90
  requireString(record.productTypeId, `${what}.productTypeId`, fail);
93
91
  requireString(record.displayName, `${what}.displayName`, fail);
94
92
  requireNumber(record.boost, `${what}.boost`, fail);
95
- }
96
- function checkCategoryInferences(items, what, fail) {
97
- items.forEach((item, index) => checkCategoryInference(item, `${what}[${index}]`, fail));
98
- }
93
+ };
94
+ const checkCategoryInferences = (items, what, fail) => {
95
+ items.forEach((item, index) => {
96
+ checkCategoryInference(item, `${what}[${index}]`, fail);
97
+ });
98
+ };
99
99
  /** Check one detected surface — identity, function, confidence, and its box. */
100
- function checkDetectedSurface(value, what, fail) {
100
+ const checkDetectedSurface = (value, what, fail) => {
101
101
  const record = requireRecord(value, what, fail);
102
102
  requireString(record.id, `${what}.id`, fail);
103
103
  requireString(record.label, `${what}.label`, fail);
@@ -110,12 +110,12 @@ function checkDetectedSurface(value, what, fail) {
110
110
  for (const corner of BOUNDING_BOX_CORNERS) {
111
111
  requireNumber(box[corner], `${what}.boundingBox.${corner}`, fail);
112
112
  }
113
- }
113
+ };
114
114
  /**
115
115
  * Check one surface's matching state and fill the canonical `.default([])`
116
116
  * arrays, so a consumer never meets `undefined` where the type promises an array.
117
117
  */
118
- function readSurfaceResult(value, what, fail) {
118
+ const readSurfaceResult = (value, what, fail) => {
119
119
  const record = requireRecord(value, what, fail);
120
120
  checkDetectedSurface(record.surface, `${what}.surface`, fail);
121
121
  requireEnum(record.status, MATCH_SURFACE_STATUSES, `${what}.status`, fail);
@@ -124,8 +124,8 @@ function readSurfaceResult(value, what, fail) {
124
124
  checkSearchResultItems(results, `${what}.results`, fail);
125
125
  const inferredCategories = requireDefaultedArray(record.inferredCategories, `${what}.inferredCategories`, fail);
126
126
  checkCategoryInferences(inferredCategories, `${what}.inferredCategories`, fail);
127
- return { ...record, results, inferredCategories };
128
- }
127
+ return { ...record, inferredCategories, results };
128
+ };
129
129
  /**
130
130
  * `matchRunTotalsSchema` — four non-negative integers plus the TWO cross-field
131
131
  * invariants its `.superRefine()` enforces. Relational rather than field-wise,
@@ -135,7 +135,7 @@ function readSurfaceResult(value, what, fail) {
135
135
  * `matchesSoFar` is deliberately unbounded against the surface counts — one
136
136
  * surface can carry many results and another none, so no ratio is principled.
137
137
  */
138
- function checkRunTotals(value, what, fail) {
138
+ const checkRunTotals = (value, what, fail) => {
139
139
  const record = requireRecord(value, what, fail);
140
140
  const detected = requireIntegerAtLeast(record.surfacesDetected, `${what}.surfacesDetected`, fail, 0);
141
141
  const resolved = requireIntegerAtLeast(record.surfacesResolved, `${what}.surfacesResolved`, fail, 0);
@@ -149,9 +149,9 @@ function checkRunTotals(value, what, fail) {
149
149
  if (resolved > detected) {
150
150
  throw fail(`${what}.surfacesResolved (${resolved}) exceeds ${what}.surfacesDetected (${detected}) — a run cannot resolve more surfaces than it found`);
151
151
  }
152
- }
152
+ };
153
153
  /** Check the terminal workflow result — image type, surfaces, optional roll-ups. */
154
- function readWorkflowResult(value, what, fail) {
154
+ const readWorkflowResult = (value, what, fail) => {
155
155
  const record = requireRecord(value, what, fail);
156
156
  requireEnum(record.imageType, MATCH_IMAGE_TYPES, `${what}.imageType`, fail);
157
157
  const surfaces = requireArray(record.surfaces, `${what}.surfaces`, fail).map((surface, index) => readSurfaceResult(surface, `${what}.surfaces[${index}]`, fail));
@@ -164,26 +164,30 @@ function readWorkflowResult(value, what, fail) {
164
164
  checkCategoryInferences(requireArray(record.inferredCategories, `${what}.inferredCategories`, fail), `${what}.inferredCategories`, fail);
165
165
  }
166
166
  return { ...record, surfaces };
167
- }
167
+ };
168
168
  /**
169
169
  * Validate one frame against the canonical contract and apply the schema's
170
170
  * `.default([])` fills, returning the event as a plain record. Unknown keys ride
171
171
  * through untouched — see the pass-through note in the module header.
172
172
  */
173
- function readFrame(value, fail) {
173
+ const readFrame = (value, fail) => {
174
174
  const record = requireRecord(value, "the frame", fail);
175
175
  const type = requireString(record.type, "`type`", fail);
176
176
  switch (type) {
177
- case "step":
177
+ case "step": {
178
178
  requireEnum(record.step, MATCH_WORKFLOW_STEPS, "`step`", fail);
179
179
  requireString(record.message, "`message`", fail);
180
180
  return record;
181
- case "classified":
181
+ }
182
+ case "classified": {
182
183
  requireEnum(record.imageType, MATCH_IMAGE_TYPES, "`imageType`", fail);
183
184
  return record;
185
+ }
184
186
  case "surfaces": {
185
187
  const surfaces = requireArray(record.surfaces, "`surfaces`", fail);
186
- surfaces.forEach((surface, index) => checkDetectedSurface(surface, `surfaces[${index}]`, fail));
188
+ surfaces.forEach((surface, index) => {
189
+ checkDetectedSurface(surface, `surfaces[${index}]`, fail);
190
+ });
187
191
  return record;
188
192
  }
189
193
  case "surface": {
@@ -193,37 +197,48 @@ function readFrame(value, fail) {
193
197
  checkRunTotals(record.totals, "`totals`", fail);
194
198
  return { ...record, surface };
195
199
  }
196
- case "heartbeat":
200
+ case "heartbeat": {
197
201
  // Payload-free by contract — there is nothing else to check.
198
202
  return record;
199
- case "complete":
203
+ }
204
+ case "complete": {
200
205
  return {
201
206
  ...record,
202
207
  result: readWorkflowResult(record.result, "`result`", fail),
203
208
  };
204
- case "error":
209
+ }
210
+ case "error": {
205
211
  requireString(record.message, "`message`", fail);
212
+ if (record.reason !== undefined) {
213
+ requireEnum(record.reason, MATCH_ERROR_REASONS, "`reason`", fail);
214
+ }
215
+ if (record.retriable !== undefined &&
216
+ typeof record.retriable !== "boolean") {
217
+ throw fail("`retriable` must be a boolean");
218
+ }
206
219
  return record;
207
- default:
220
+ }
221
+ default: {
208
222
  throw fail(`\`type\` is not a known Match stream event (${type})`);
223
+ }
209
224
  }
210
- }
225
+ };
211
226
  /**
212
227
  * The single narrowing boundary of this module. {@link readFrame} has already
213
228
  * proven every field the union's shape depends on; re-checking the discriminant
214
229
  * here keeps the assertion from being vacuous.
215
230
  */
216
- function assertMatchStreamEvent(frame, fail) {
231
+ const assertMatchStreamEvent = (frame, fail) => {
217
232
  const record = requireRecord(frame, "the frame", fail);
218
233
  requireEnum(record.type, MATCH_STREAM_EVENT_TYPES, "`type`", fail);
219
- }
234
+ };
220
235
  /**
221
236
  * Decode one NDJSON line into a validated {@link MatchStreamEvent}, raising a
222
237
  * {@link MaterialGraphError} on anything the contract does not allow. Exported
223
238
  * for `contract-parity.test.ts`, which re-checks it against the real Zod schema;
224
239
  * it is not part of the published surface.
225
240
  */
226
- export function decodeMatchStreamEvent(line) {
241
+ export const decodeMatchStreamEvent = (line) => {
227
242
  const fail = frameFailure(line);
228
243
  let parsed;
229
244
  try {
@@ -235,10 +250,8 @@ export function decodeMatchStreamEvent(line) {
235
250
  const frame = readFrame(parsed, fail);
236
251
  assertMatchStreamEvent(frame, fail);
237
252
  return frame;
238
- }
239
- function isTerminal(event) {
240
- return event.type === "complete" || event.type === "error";
241
- }
253
+ };
254
+ const isTerminal = (event) => event.type === "complete" || event.type === "error";
242
255
  /**
243
256
  * Stream Match progress as an async iterator. Iterates until the run reaches a
244
257
  * `complete` or an `error` event, and raises if the connection ends before one
@@ -251,33 +264,38 @@ function isTerminal(event) {
251
264
  * it cancelled, so the truncated-stream error is not raised for it.
252
265
  */
253
266
  export async function* matchStream(transport, input, signal) {
254
- if (signal?.aborted === true)
267
+ if (signal?.aborted === true) {
255
268
  return;
269
+ }
256
270
  // Our own controller, so abandoning the iterator cancels the request too.
257
271
  const controller = new AbortController();
258
- const forwardAbort = () => controller.abort();
272
+ const forwardAbort = () => {
273
+ controller.abort();
274
+ };
259
275
  signal?.addEventListener("abort", forwardAbort, { once: true });
260
276
  try {
261
277
  let body;
262
278
  try {
263
279
  body = await transport.openStream(operation("match_material"), {
280
+ accept: MATCH_STREAM_ACCEPT,
264
281
  body: input,
265
282
  signal: controller.signal,
266
- accept: MATCH_STREAM_ACCEPT,
267
283
  subject: "match stream",
268
284
  });
269
285
  }
270
286
  catch (error) {
271
287
  // Cancelled before the response headers landed — the caller's own stop.
272
- if (controller.signal.aborted)
288
+ if (controller.signal.aborted) {
273
289
  return;
290
+ }
274
291
  throw error;
275
292
  }
276
293
  for await (const line of readNdjsonLines(body, controller.signal)) {
277
294
  const event = decodeMatchStreamEvent(line);
278
295
  yield event;
279
- if (isTerminal(event))
296
+ if (isTerminal(event)) {
280
297
  return;
298
+ }
281
299
  }
282
300
  if (!controller.signal.aborted) {
283
301
  throw MaterialGraphError.unexpected("MaterialGraph match stream ended without a `complete` or `error` event — the connection was truncated");
package/match-types.d.ts CHANGED
@@ -9,14 +9,15 @@ import type { CorpusScope } from "./corpus-scope-types.js";
9
9
  * along its natural seam: everything a caller of `client.match()` needs, and
10
10
  * nothing else. The STREAMING contracts live in `match-stream-types.ts`.
11
11
  */
12
+ import type { MatchWorkflowResult } from "./match-stream-types.js";
12
13
  import type { SearchFilter } from "./search-refinement-types.js";
13
14
  import type { OpenEnum, PaletteSwatch, SearchResultItem } from "./types.js";
14
15
  /** An inferred product-type category from the query image, with its boost. */
15
- export interface MatchCategoryInference {
16
+ export type MatchCategoryInference = {
16
17
  productTypeId: string;
17
18
  displayName: string;
18
19
  boost: number;
19
- }
20
+ };
20
21
  /**
21
22
  * The Material Match response (non-streaming form).
22
23
  *
@@ -31,13 +32,13 @@ export interface MatchCategoryInference {
31
32
  * means the material carries no palette on record — UNKNOWN, never "no colour"
32
33
  * and never "the wrong colour".
33
34
  */
34
- export interface MatchColourReading {
35
+ export type MatchColourReading = {
35
36
  variantId: string;
36
37
  colourDistance: number | null;
37
38
  colourBand: string | null;
38
39
  measurable: boolean;
39
- }
40
- export interface MatchResponse {
40
+ };
41
+ export type MatchResponse = {
41
42
  /**
42
43
  * Result attribution (MG-1131). `basis` is always `"match"` here — this route
43
44
  * has no `mode`, so this is the field that lets a Match outcome be grouped
@@ -73,9 +74,32 @@ export interface MatchResponse {
73
74
  /** The query crop's own extracted palette — what the colour stage compared against. */
74
75
  segmentPalette?: PaletteSwatch[] | null;
75
76
  note?: string;
76
- }
77
+ };
78
+ /**
79
+ * What a BUFFERED `match()` actually answers with, either shape.
80
+ *
81
+ * An IMAGE input (`imageBase64` or `imageUrl`) runs the same workflow the
82
+ * stream lane runs — image kind first, then the swatch shortcut or the scene
83
+ * route — and answers in {@link MatchWorkflowResult}. A `vector` input has no
84
+ * bytes to read a kind from and keeps the thin {@link MatchResponse} leaf.
85
+ * The server declares exactly this as an `anyOf` of the two.
86
+ *
87
+ * TELL THEM APART BY A FIELD THAT ONLY ONE HAS, never by what you sent:
88
+ * `imageType` is required on the workflow shape and absent from the leaf, and
89
+ * `qualityFloor` is required on the leaf and absent from the workflow shape.
90
+ *
91
+ * ```ts
92
+ * const answer = await client.match({ imageBase64 });
93
+ * if ("imageType" in answer) {
94
+ * for (const surface of answer.surfaces) render(surface);
95
+ * } else {
96
+ * report(answer.qualityFloor, answer.droppedBelowFloor);
97
+ * }
98
+ * ```
99
+ */
100
+ export type MatchAnswer = MatchResponse | MatchWorkflowResult;
77
101
  /** The Material Match request body — exactly one of `imageBase64`, `imageUrl`, `vector`. */
78
- export interface MatchInput {
102
+ export type MatchInput = {
79
103
  imageBase64?: string;
80
104
  imageUrl?: string;
81
105
  vector?: string;
@@ -111,4 +135,4 @@ export interface MatchInput {
111
135
  preferences?: SearchFilter[];
112
136
  /** Opt in to seed-relative verdict cells; inert unless the inner search carries a variant seed. */
113
137
  verdicts?: boolean;
114
- }
138
+ };