@instruments/materialgraph 0.30.0 → 0.32.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.
@@ -1,65 +0,0 @@
1
- /** How the workflow classified the uploaded image. */
2
- export const MATCH_IMAGE_TYPES = [
3
- "swatch",
4
- "installation",
5
- "flatplan",
6
- ];
7
- /** Why a Match run ended on an `error` frame (MG-730). */
8
- export const MATCH_ERROR_REASONS = ["upstream_unavailable", "failed"];
9
- /** What a detected surface is FOR — the element the material would serve. */
10
- export const MATCH_MATERIAL_FUNCTIONS = [
11
- "flooring",
12
- "wall_surface",
13
- "ceiling",
14
- "partition",
15
- "upholstery",
16
- "millwork",
17
- "countertop",
18
- "accent",
19
- "sample",
20
- ];
21
- /** The workflow's coarse progress phase, narrated by a `step` event. */
22
- export const MATCH_WORKFLOW_STEPS = [
23
- "classifying",
24
- "decomposing",
25
- "matching",
26
- "complete",
27
- ];
28
- /**
29
- * Where one surface stands in the run.
30
- *
31
- * `colour_fallback` (appended, additive): the surface completed, but through
32
- * the colour lane — its visible sample was too small to read texture, so the
33
- * results match the region's palette, scoped to the detected category. Such a
34
- * surface carries a `note` saying so and no `senseCheck` verdicts (there is no
35
- * texture to verify).
36
- */
37
- export const MATCH_SURFACE_STATUSES = [
38
- "pending",
39
- "matching",
40
- "complete",
41
- "error",
42
- "colour_fallback",
43
- ];
44
- /** Every frame type the stream can carry — the union's discriminant values. */
45
- export const MATCH_STREAM_EVENT_TYPES = [
46
- "step",
47
- "classified",
48
- "surfaces",
49
- "surface",
50
- "heartbeat",
51
- "complete",
52
- "error",
53
- ];
54
- export const MATCH_SENSE_CHECK_VERDICTS = [
55
- "confirmed",
56
- "rejected",
57
- "abstained",
58
- ];
59
- /** The terminal Match workflow result — every surface in its final state. */
60
- /**
61
- * How a Match run ended. `no_surfaces` means the detector worked and found no
62
- * material in the photo — a finding, not a failure. Branch on this rather than
63
- * on an error message. Absent means `matched`.
64
- */
65
- export const MATCH_OUTCOMES = ["matched", "no_surfaces"];
package/match-stream.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import type { HttpTransport } from "./http-transport.js";
2
- import type { MatchStreamEvent } from "./match-stream-types.js";
3
- import type { MatchInput } from "./match-types.js";
4
- /**
5
- * Decode one NDJSON line into a validated {@link MatchStreamEvent}, raising a
6
- * {@link MaterialGraphError} on anything the contract does not allow. Exported
7
- * for `contract-parity.test.ts`, which re-checks it against the real Zod schema;
8
- * it is not part of the published surface.
9
- */
10
- export declare const decodeMatchStreamEvent: (line: string) => MatchStreamEvent;
11
- /**
12
- * Stream Match progress as an async iterator. Iterates until the run reaches a
13
- * `complete` or an `error` event, and raises if the connection ends before one
14
- * arrives.
15
- *
16
- * `signal` genuinely cancels: it is linked to the AbortController that owns the
17
- * in-flight request, so aborting (or simply `break`ing out of the loop) tears
18
- * down the HTTP connection rather than merely stopping the yields. A
19
- * caller-requested abort ends the iteration CLEANLY — the caller already knows
20
- * it cancelled, so the truncated-stream error is not raised for it.
21
- */
22
- export declare function matchStream(transport: HttpTransport, input: MatchInput, signal?: AbortSignal): AsyncGenerator<MatchStreamEvent, void, unknown>;
package/match-stream.js DELETED
@@ -1,308 +0,0 @@
1
- /**
2
- * The Material Match PROGRESS STREAM — the streaming twin of the buffered
3
- * `match()` read. `POST /api/v1/search/match` with `Accept:
4
- * application/x-ndjson` runs classify → decompose → one match per surface and
5
- * emits an event per line as each surface resolves, so a `/match` UI can fill in
6
- * progressively instead of showing a spinner until the whole run lands.
7
- *
8
- * FAIL-FAST BY CONTRACT. Every non-blank line is validated before it is yielded;
9
- * a line that is not JSON, is not an object, carries an unknown `type`, an
10
- * out-of-range number, a value outside a closed enum, or a field of the wrong
11
- * kind raises a {@link MaterialGraphError} rather than being skipped. A stream
12
- * that ends without a `complete` or an `error` event was truncated, and raises
13
- * too — a short run and a dropped connection must never look alike. There is no
14
- * retry or reconnect.
15
- *
16
- * HEARTBEATS ARE NOT PROGRESS. A scene match goes quiet for long stretches
17
- * between surfaces, and proxies kill idle connections, so the server sends a
18
- * payload-free `{ type: "heartbeat" }` when the stream has been silent. It is
19
- * yielded like any other event — the connection being alive is worth knowing —
20
- * but it carries nothing and marks no state change: never advance a step, reset
21
- * a no-progress watchdog, or count anything on one. It is not terminal, and none
22
- * arrives after `complete` or `error`.
23
- *
24
- * ACCEPTANCE PARITY WITH THE CANONICAL SCHEMA. The SDK cannot import
25
- * `matchStreamEventSchema` at runtime (it publishes with zero dependencies), so
26
- * this validator mirrors it by hand and `contract-parity.test.ts` proves the two
27
- * ACCEPT and REJECT the same frames — with the enum members derived from the
28
- * canonical schemas, so a server-side addition fails that test.
29
- *
30
- * OUTPUT IS PASS-THROUGH, NOT `parse` OUTPUT — a deliberate divergence. Zod
31
- * objects strip unknown keys, so `matchStreamEventSchema.parse(frame)` DROPS any
32
- * field the schema does not name. This reader keeps the frame as sent. Three
33
- * reasons:
34
- * 1. Nothing on the emit path parses. The route stringifies the workflow's own
35
- * objects (`createMatchNdjsonStream`), so `parse` output is not what the wire
36
- * carries; reshaping to match it would invent a normalisation that never
37
- * happened.
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.
41
- * 3. An additive server field then reaches consumers without an SDK release,
42
- * which matters while new EVENT types still require one (MG-658).
43
- * The ONE exception is `.default([])`: an omitted `results` / `inferredCategories`
44
- * is FILLED to `[]`, because {@link MatchSurfaceResult} types both as always
45
- * present and the SDK must not hand back a type it has falsified.
46
- *
47
- * VALIDATION DEPTH — FULL. The frame envelope, every closed enum, every bounded
48
- * number, every non-negative integer in a `surface` event's `totals`, every
49
- * optional string, `MatchCategoryInference` items, and every
50
- * `SearchResultItem` in `results[]` are checked exhaustively. Item validation is
51
- * the shared `checkSearchResultItem` (`search-result-validation.ts`), which
52
- * mirrors `searchResultItemSchema` and its whole closure.
53
- */
54
- import { MaterialGraphError } from "./errors.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";
56
- import { readNdjsonLines } from "./ndjson.js";
57
- import { findSdkOperation } from "./operations.js";
58
- import { checkSearchResultItems } from "./search-result-validation.js";
59
- import { requireArray, requireBoundedNumber, requireDefaultedArray, requireEnum, requireIntegerAtLeast, requireNumber, requireOptionalString, requireRecord, requireString, } from "./validation.js";
60
- /** The NDJSON media type the Match route negotiates the scene workflow on. */
61
- const MATCH_STREAM_ACCEPT = "application/x-ndjson";
62
- /** How much of an offending frame a diagnostic quotes (crops can be megabytes). */
63
- const MAX_FRAME_PREVIEW = 200;
64
- const BOUNDING_BOX_CORNERS = Object.freeze([
65
- "x0",
66
- "y0",
67
- "x1",
68
- "y1",
69
- ]);
70
- const operation = (operationId) => {
71
- const found = findSdkOperation(operationId);
72
- if (!found) {
73
- throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
74
- }
75
- return found;
76
- };
77
- /**
78
- * The failure shape every check in this module raises: one message naming the
79
- * offending path and quoting a bounded slice of the frame.
80
- */
81
- const frameFailure = (line) => {
82
- const preview = line.length > MAX_FRAME_PREVIEW
83
- ? `${line.slice(0, MAX_FRAME_PREVIEW)}…`
84
- : line;
85
- return (reason) => MaterialGraphError.unexpected(`MaterialGraph match stream sent a malformed NDJSON frame: ${reason}. Frame: ${preview}`);
86
- };
87
- /** Check one `MatchCategoryInference` item. */
88
- const checkCategoryInference = (value, what, fail) => {
89
- const record = requireRecord(value, what, fail);
90
- requireString(record.productTypeId, `${what}.productTypeId`, fail);
91
- requireString(record.displayName, `${what}.displayName`, fail);
92
- requireNumber(record.boost, `${what}.boost`, fail);
93
- };
94
- const checkCategoryInferences = (items, what, fail) => {
95
- items.forEach((item, index) => {
96
- checkCategoryInference(item, `${what}[${index}]`, fail);
97
- });
98
- };
99
- /** Check one detected surface — identity, function, confidence, and its box. */
100
- const checkDetectedSurface = (value, what, fail) => {
101
- const record = requireRecord(value, what, fail);
102
- requireString(record.id, `${what}.id`, fail);
103
- requireString(record.label, `${what}.label`, fail);
104
- requireEnum(record.materialFunction, MATCH_MATERIAL_FUNCTIONS, `${what}.materialFunction`, fail);
105
- requireOptionalString(record.categoryHint, `${what}.categoryHint`, fail);
106
- requireOptionalString(record.description, `${what}.description`, fail);
107
- requireOptionalString(record.cropDataUri, `${what}.cropDataUri`, fail);
108
- requireBoundedNumber(record.confidence, `${what}.confidence`, fail, 0, 1);
109
- const box = requireRecord(record.boundingBox, `${what}.boundingBox`, fail);
110
- for (const corner of BOUNDING_BOX_CORNERS) {
111
- requireNumber(box[corner], `${what}.boundingBox.${corner}`, fail);
112
- }
113
- };
114
- /**
115
- * Check one surface's matching state and fill the canonical `.default([])`
116
- * arrays, so a consumer never meets `undefined` where the type promises an array.
117
- */
118
- const readSurfaceResult = (value, what, fail) => {
119
- const record = requireRecord(value, what, fail);
120
- checkDetectedSurface(record.surface, `${what}.surface`, fail);
121
- requireEnum(record.status, MATCH_SURFACE_STATUSES, `${what}.status`, fail);
122
- requireOptionalString(record.error, `${what}.error`, fail);
123
- const results = requireDefaultedArray(record.results, `${what}.results`, fail);
124
- checkSearchResultItems(results, `${what}.results`, fail);
125
- const inferredCategories = requireDefaultedArray(record.inferredCategories, `${what}.inferredCategories`, fail);
126
- checkCategoryInferences(inferredCategories, `${what}.inferredCategories`, fail);
127
- return { ...record, inferredCategories, results };
128
- };
129
- /**
130
- * `matchRunTotalsSchema` — four non-negative integers plus the TWO cross-field
131
- * invariants its `.superRefine()` enforces. Relational rather than field-wise,
132
- * which is no obstacle here: they are two comparisons, and without them a frame
133
- * claiming "7 of 2 surfaces done" would reach a consumer that trusted the tally.
134
- *
135
- * `matchesSoFar` is deliberately unbounded against the surface counts — one
136
- * surface can carry many results and another none, so no ratio is principled.
137
- */
138
- const checkRunTotals = (value, what, fail) => {
139
- const record = requireRecord(value, what, fail);
140
- const detected = requireIntegerAtLeast(record.surfacesDetected, `${what}.surfacesDetected`, fail, 0);
141
- const resolved = requireIntegerAtLeast(record.surfacesResolved, `${what}.surfacesResolved`, fail, 0);
142
- const failed = requireIntegerAtLeast(record.surfacesFailed, `${what}.surfacesFailed`, fail, 0);
143
- requireIntegerAtLeast(record.matchesSoFar, `${what}.matchesSoFar`, fail, 0);
144
- // superRefine invariant 1 — a failed surface is also a resolved one.
145
- if (failed > resolved) {
146
- throw fail(`${what}.surfacesFailed (${failed}) exceeds ${what}.surfacesResolved (${resolved}) — a failed surface is also a resolved one`);
147
- }
148
- // superRefine invariant 2 — a run cannot resolve more surfaces than it found.
149
- if (resolved > detected) {
150
- throw fail(`${what}.surfacesResolved (${resolved}) exceeds ${what}.surfacesDetected (${detected}) — a run cannot resolve more surfaces than it found`);
151
- }
152
- };
153
- /** Check the terminal workflow result — image type, surfaces, optional roll-ups. */
154
- const readWorkflowResult = (value, what, fail) => {
155
- const record = requireRecord(value, what, fail);
156
- requireEnum(record.imageType, MATCH_IMAGE_TYPES, `${what}.imageType`, fail);
157
- const surfaces = requireArray(record.surfaces, `${what}.surfaces`, fail).map((surface, index) => readSurfaceResult(surface, `${what}.surfaces[${index}]`, fail));
158
- // These two are `.optional()` on the workflow result, NOT `.default([])`:
159
- // absent stays absent, matching the swatch path that skips decompose.
160
- if (record.results !== undefined) {
161
- checkSearchResultItems(requireArray(record.results, `${what}.results`, fail), `${what}.results`, fail);
162
- }
163
- if (record.inferredCategories !== undefined) {
164
- checkCategoryInferences(requireArray(record.inferredCategories, `${what}.inferredCategories`, fail), `${what}.inferredCategories`, fail);
165
- }
166
- return { ...record, surfaces };
167
- };
168
- /**
169
- * Validate one frame against the canonical contract and apply the schema's
170
- * `.default([])` fills, returning the event as a plain record. Unknown keys ride
171
- * through untouched — see the pass-through note in the module header.
172
- */
173
- const readFrame = (value, fail) => {
174
- const record = requireRecord(value, "the frame", fail);
175
- const type = requireString(record.type, "`type`", fail);
176
- switch (type) {
177
- case "step": {
178
- requireEnum(record.step, MATCH_WORKFLOW_STEPS, "`step`", fail);
179
- requireString(record.message, "`message`", fail);
180
- return record;
181
- }
182
- case "classified": {
183
- requireEnum(record.imageType, MATCH_IMAGE_TYPES, "`imageType`", fail);
184
- return record;
185
- }
186
- case "surfaces": {
187
- const surfaces = requireArray(record.surfaces, "`surfaces`", fail);
188
- surfaces.forEach((surface, index) => {
189
- checkDetectedSurface(surface, `surfaces[${index}]`, fail);
190
- });
191
- return record;
192
- }
193
- case "surface": {
194
- // Payload before tally, so a malformed surface reports itself rather than
195
- // being masked by whatever the totals looked like.
196
- const surface = readSurfaceResult(record.surface, "`surface`", fail);
197
- checkRunTotals(record.totals, "`totals`", fail);
198
- return { ...record, surface };
199
- }
200
- case "heartbeat": {
201
- // Payload-free by contract — there is nothing else to check.
202
- return record;
203
- }
204
- case "complete": {
205
- return {
206
- ...record,
207
- result: readWorkflowResult(record.result, "`result`", fail),
208
- };
209
- }
210
- case "error": {
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
- }
219
- return record;
220
- }
221
- default: {
222
- throw fail(`\`type\` is not a known Match stream event (${type})`);
223
- }
224
- }
225
- };
226
- /**
227
- * The single narrowing boundary of this module. {@link readFrame} has already
228
- * proven every field the union's shape depends on; re-checking the discriminant
229
- * here keeps the assertion from being vacuous.
230
- */
231
- const assertMatchStreamEvent = (frame, fail) => {
232
- const record = requireRecord(frame, "the frame", fail);
233
- requireEnum(record.type, MATCH_STREAM_EVENT_TYPES, "`type`", fail);
234
- };
235
- /**
236
- * Decode one NDJSON line into a validated {@link MatchStreamEvent}, raising a
237
- * {@link MaterialGraphError} on anything the contract does not allow. Exported
238
- * for `contract-parity.test.ts`, which re-checks it against the real Zod schema;
239
- * it is not part of the published surface.
240
- */
241
- export const decodeMatchStreamEvent = (line) => {
242
- const fail = frameFailure(line);
243
- let parsed;
244
- try {
245
- parsed = JSON.parse(line);
246
- }
247
- catch (cause) {
248
- throw fail(`it is not valid JSON (${String(cause)})`);
249
- }
250
- const frame = readFrame(parsed, fail);
251
- assertMatchStreamEvent(frame, fail);
252
- return frame;
253
- };
254
- const isTerminal = (event) => event.type === "complete" || event.type === "error";
255
- /**
256
- * Stream Match progress as an async iterator. Iterates until the run reaches a
257
- * `complete` or an `error` event, and raises if the connection ends before one
258
- * arrives.
259
- *
260
- * `signal` genuinely cancels: it is linked to the AbortController that owns the
261
- * in-flight request, so aborting (or simply `break`ing out of the loop) tears
262
- * down the HTTP connection rather than merely stopping the yields. A
263
- * caller-requested abort ends the iteration CLEANLY — the caller already knows
264
- * it cancelled, so the truncated-stream error is not raised for it.
265
- */
266
- export async function* matchStream(transport, input, signal) {
267
- if (signal?.aborted === true) {
268
- return;
269
- }
270
- // Our own controller, so abandoning the iterator cancels the request too.
271
- const controller = new AbortController();
272
- const forwardAbort = () => {
273
- controller.abort();
274
- };
275
- signal?.addEventListener("abort", forwardAbort, { once: true });
276
- try {
277
- let body;
278
- try {
279
- body = await transport.openStream(operation("match_material"), {
280
- accept: MATCH_STREAM_ACCEPT,
281
- body: input,
282
- signal: controller.signal,
283
- subject: "match stream",
284
- });
285
- }
286
- catch (error) {
287
- // Cancelled before the response headers landed — the caller's own stop.
288
- if (controller.signal.aborted) {
289
- return;
290
- }
291
- throw error;
292
- }
293
- for await (const line of readNdjsonLines(body, controller.signal)) {
294
- const event = decodeMatchStreamEvent(line);
295
- yield event;
296
- if (isTerminal(event)) {
297
- return;
298
- }
299
- }
300
- if (!controller.signal.aborted) {
301
- throw MaterialGraphError.unexpected("MaterialGraph match stream ended without a `complete` or `error` event — the connection was truncated");
302
- }
303
- }
304
- finally {
305
- signal?.removeEventListener("abort", forwardAbort);
306
- controller.abort();
307
- }
308
- }
package/match-types.d.ts DELETED
@@ -1,138 +0,0 @@
1
- import type { ResponseAttribution } from "./attribution-types.js";
2
- import type { CorpusScope } from "./corpus-scope-types.js";
3
- /**
4
- * @module sdk/match-types
5
- * @description The buffered Material Match request and response, hand-mirrored
6
- * from the server's Zod contracts.
7
- *
8
- * Split out of `types.ts` — which is at the repository's source-size limit —
9
- * along its natural seam: everything a caller of `client.match()` needs, and
10
- * nothing else. The STREAMING contracts live in `match-stream-types.ts`.
11
- */
12
- import type { MatchWorkflowResult } from "./match-stream-types.js";
13
- import type { SearchFilter } from "./search-refinement-types.js";
14
- import type { OpenEnum, PaletteSwatch, SearchResultItem } from "./types.js";
15
- /** An inferred product-type category from the query image, with its boost. */
16
- export type MatchCategoryInference = {
17
- productTypeId: string;
18
- displayName: string;
19
- boost: number;
20
- };
21
- /**
22
- * The Material Match response (non-streaming form).
23
- *
24
- * `poolSize` is the candidate pool the floor was applied to, NOT a corpus total
25
- * and NOT "how many materials look like this". An empty `results` with
26
- * `droppedBelowFloor > 0` means "candidates existed and none was good enough" —
27
- * a different answer from `poolSize: 0`, which means the image found nothing to
28
- * search against at all.
29
- */
30
- /**
31
- * One candidate's colour reading against the query crop. `measurable: false`
32
- * means the material carries no palette on record — UNKNOWN, never "no colour"
33
- * and never "the wrong colour".
34
- */
35
- export type MatchColourReading = {
36
- variantId: string;
37
- colourDistance: number | null;
38
- colourBand: string | null;
39
- measurable: boolean;
40
- };
41
- export type MatchResponse = {
42
- /**
43
- * Result attribution (MG-1131). `basis` is always `"match"` here — this route
44
- * has no `mode`, so this is the field that lets a Match outcome be grouped
45
- * alongside a `search()` one. Store it beside the query and each item's
46
- * `resultId` beside the outcome.
47
- *
48
- * ABSENT only from a response produced by a server predating MG-1131.
49
- */
50
- attribution?: ResponseAttribution;
51
- /** THIS answer, capped by `limit`. Not a total. */
52
- results: SearchResultItem[];
53
- inferredCategories: MatchCategoryInference[];
54
- qualityFloor: number;
55
- poolSize: number;
56
- droppedBelowFloor: number;
57
- droppedAsDuplicateProduct: number;
58
- /**
59
- * The closest candidate the quality floor REJECTED, in the same [0,1] unit as
60
- * `qualityFloor`. This is what turns "no matches" into "the closest thing in
61
- * the catalogue read 0.48 against a 0.55 bar" — the one number that separates
62
- * an engine that failed from a corpus that does not hold the material. Absent
63
- * when nothing was rejected; null when the rejects carried no readable signal.
64
- */
65
- bestRejected?: number | null;
66
- /** The maximum score achievable under the weights this query ran — `score`'s denominator. */
67
- maxScore: number;
68
- /**
69
- * Per-candidate colour readings against the query crop, in `results` order.
70
- * `null` means the colour stage DID NOT RUN (no image bytes, or it was turned
71
- * off) — never "no colour".
72
- */
73
- colour?: MatchColourReading[] | null;
74
- /** The query crop's own extracted palette — what the colour stage compared against. */
75
- segmentPalette?: PaletteSwatch[] | null;
76
- note?: string;
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;
101
- /** The Material Match request body — exactly one of `imageBase64`, `imageUrl`, `vector`. */
102
- export type MatchInput = {
103
- imageBase64?: string;
104
- imageUrl?: string;
105
- vector?: string;
106
- limit?: number;
107
- qualityFloor?: number;
108
- categoryTopK?: number;
109
- categoryMinBoost?: number;
110
- detectionLabel?: string;
111
- /**
112
- * The detector's fuller reading of the surface — a SENTENCE, where
113
- * `detectionLabel` is a phrase. Fused WITH the image into ONE multimodal
114
- * query vector and preferred over the label when both are present.
115
- *
116
- * Distinct from `detectionLabel`, which also buys the category scope: a
117
- * request can narrow the scope by label and steer retrieval by description in
118
- * the same call.
119
- */
120
- detectionDescription?: string;
121
- materialFunction?: string;
122
- detectionCategory?: string;
123
- /**
124
- * Opt-in: also return quality-floor rejects, flagged `belowFloor: true` and
125
- * appended after the kept results. Absent/false = default behaviour;
126
- * `droppedBelowFloor` is unchanged.
127
- */
128
- includeBelowFloor?: boolean;
129
- requestedType?: OpenEnum<"swatch" | "installation" | "flatplan">;
130
- /** Corpus to retrieve candidates from, applied inside retrieval. */
131
- corpusScope?: CorpusScope;
132
- /** Hard spec gates forwarded verbatim to the inner search — term match or presence, each with an optional trust bar. */
133
- filters?: SearchFilter[];
134
- /** Soft criteria (rank-never-exclude). A miss demotes within the page but never removes a hit. */
135
- preferences?: SearchFilter[];
136
- /** Opt in to seed-relative verdict cells; inert unless the inner search carries a variant seed. */
137
- verdicts?: boolean;
138
- };
package/match-types.js DELETED
@@ -1 +0,0 @@
1
- export {};