@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 +141 -7
- package/client.js +185 -125
- package/codes-types.d.ts +93 -0
- package/codes-types.js +1 -0
- package/composition-types.d.ts +117 -0
- package/composition-types.js +10 -0
- package/http-transport.d.ts +57 -0
- package/http-transport.js +131 -0
- package/index.d.ts +12 -0
- package/index.js +6 -0
- package/operations.d.ts +6 -2
- package/operations.js +24 -2
- package/package.json +1 -1
- package/project-types.d.ts +67 -0
- package/project-types.js +16 -0
- package/projects.d.ts +13 -0
- package/projects.js +46 -0
- package/run-events.d.ts +82 -0
- package/run-events.js +29 -0
- package/run-types.d.ts +202 -0
- package/run-types.js +6 -0
- package/runs.d.ts +42 -0
- package/runs.js +214 -0
- package/search-refinement-types.d.ts +13 -1
- package/spec-types.d.ts +78 -0
- package/spec-types.js +40 -0
- package/specifications.d.ts +63 -0
- package/specifications.js +73 -0
- package/sse.d.ts +35 -0
- package/sse.js +147 -0
- package/types.d.ts +145 -4
package/spec-types.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The specification surface — the typed vocabulary for asking a material
|
|
3
|
+
* "does it meet this spec?": certifications (GREENGUARD, FloorScore, Declare),
|
|
4
|
+
* VOC / emissions testing (CDPH v1.2), sustainability programmes (LEED, EPDs),
|
|
5
|
+
* and fire / abrasion / slip classifications.
|
|
6
|
+
*
|
|
7
|
+
* Every shape here is a hand-authored, zero-dependency mirror of the server's
|
|
8
|
+
* spec filter contract (`specFilterSchema` in
|
|
9
|
+
* `@materialgraph/products/search/schema`). It exists so a caller answering a
|
|
10
|
+
* question like "find me a GREENGUARD-certified alternative" or "the same fire
|
|
11
|
+
* class, but with an EPD on record" reaches for a NAMED, typed criterion instead
|
|
12
|
+
* of hand-assembling a magic `attribute:value` string.
|
|
13
|
+
*
|
|
14
|
+
* These criteria drive two operations equally:
|
|
15
|
+
* - `client.search({ filters: [...] })` — spec gates layered onto a ranked
|
|
16
|
+
* (colour / visual / lexical) search.
|
|
17
|
+
* - `client.bySpec(...)` — the value-anchored, spec-only primitive, which takes
|
|
18
|
+
* the same criteria as `attribute:value` strings (build them with the helpers
|
|
19
|
+
* in `./specifications`).
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The virtual attribute id that matches a certification PROGRAMME wherever it
|
|
23
|
+
* landed at ingest.
|
|
24
|
+
*
|
|
25
|
+
* Certifications are not stored under one attribute: each record is an object
|
|
26
|
+
* (`{ programme, name, status, … }`) split across FOUR real object-array
|
|
27
|
+
* attributes by intent — general `certifications`,
|
|
28
|
+
* `sustainability_certifications`, `material_health_certifications`, and
|
|
29
|
+
* `voc_emissions_testing_certificate` (GREENGUARD Gold / FloorScore / CDPH land
|
|
30
|
+
* in the last). A caller shouldn't have to know which bucket a programme fell
|
|
31
|
+
* into, so this single virtual id matches the `programme` term across all four.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // "find a GREENGUARD-certified alternative"
|
|
35
|
+
* client.search({
|
|
36
|
+
* similarTo: { variantId: seed },
|
|
37
|
+
* filters: [{ attributeId: CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, term: "GREENGUARD Gold" }],
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export const CERTIFICATION_PROGRAMME_ATTRIBUTE_ID = "certification_programme";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency builders for the specification surface. Turn a plain-language
|
|
3
|
+
* spec intent into a typed criterion the SDK's two spec operations accept:
|
|
4
|
+
*
|
|
5
|
+
* - `search({ filters })` takes {@link SpecFilter} objects — a spec gate layered
|
|
6
|
+
* onto a ranked (colour / visual / lexical) search. Build them with
|
|
7
|
+
* {@link specTerm}, {@link hasSpec}, and {@link certificationProgramme}.
|
|
8
|
+
* - `bySpec(specs)` takes `attribute:value` strings — the value-anchored,
|
|
9
|
+
* spec-only primitive. Build them with {@link specCriterion} and
|
|
10
|
+
* {@link certificationProgrammeCriterion}.
|
|
11
|
+
*
|
|
12
|
+
* The point of these helpers: Desk (and any spec-driven caller) never has to
|
|
13
|
+
* remember that a certification programme lives under a VIRTUAL attribute, nor
|
|
14
|
+
* hand-format the `attribute:value` string the `bySpec` route splits on its
|
|
15
|
+
* first `:`.
|
|
16
|
+
*/
|
|
17
|
+
import { type CertificationProgrammeFilter, type SpecPresenceFilter, type SpecTermFilter } from "./spec-types.js";
|
|
18
|
+
/**
|
|
19
|
+
* A term-equality criterion for `search({ filters })` — the attribute must
|
|
20
|
+
* carry this canonical value.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // "same fire class as the seed"
|
|
24
|
+
* client.search({ similarTo: { variantId }, filters: [specTerm("fire_classifications", "astm_e84_class_a")] });
|
|
25
|
+
*/
|
|
26
|
+
export declare function specTerm(attributeId: string, term: string): SpecTermFilter;
|
|
27
|
+
/**
|
|
28
|
+
* A presence criterion for `search({ filters })` — the attribute must have ANY
|
|
29
|
+
* accepted value.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // "…and it must have an EPD on record"
|
|
33
|
+
* client.search({ similarTo: { variantId }, filters: [hasSpec("environmental_product_declaration")] });
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasSpec(attributeId: string): SpecPresenceFilter;
|
|
36
|
+
/**
|
|
37
|
+
* A certification-programme criterion for `search({ filters })`, pinned to the
|
|
38
|
+
* virtual attribute so a programme matches wherever it landed at ingest
|
|
39
|
+
* (compliance / sustainability / material-health / VOC buckets).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // "find a GREENGUARD-certified alternative"
|
|
43
|
+
* client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
|
|
44
|
+
*/
|
|
45
|
+
export declare function certificationProgramme(programme: string): CertificationProgrammeFilter;
|
|
46
|
+
/**
|
|
47
|
+
* An `attribute:value` criterion string for `bySpec`. The `bySpec` route splits
|
|
48
|
+
* on the FIRST `:`, so a value may itself contain colons; the attribute id may
|
|
49
|
+
* not.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
|
|
53
|
+
*/
|
|
54
|
+
export declare function specCriterion(attributeId: string, value: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
57
|
+
* certification PROGRAMME across all four certification buckets.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // every brand's GREENGUARD-Gold materials
|
|
61
|
+
* client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
|
|
62
|
+
*/
|
|
63
|
+
export declare function certificationProgrammeCriterion(programme: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency builders for the specification surface. Turn a plain-language
|
|
3
|
+
* spec intent into a typed criterion the SDK's two spec operations accept:
|
|
4
|
+
*
|
|
5
|
+
* - `search({ filters })` takes {@link SpecFilter} objects — a spec gate layered
|
|
6
|
+
* onto a ranked (colour / visual / lexical) search. Build them with
|
|
7
|
+
* {@link specTerm}, {@link hasSpec}, and {@link certificationProgramme}.
|
|
8
|
+
* - `bySpec(specs)` takes `attribute:value` strings — the value-anchored,
|
|
9
|
+
* spec-only primitive. Build them with {@link specCriterion} and
|
|
10
|
+
* {@link certificationProgrammeCriterion}.
|
|
11
|
+
*
|
|
12
|
+
* The point of these helpers: Desk (and any spec-driven caller) never has to
|
|
13
|
+
* remember that a certification programme lives under a VIRTUAL attribute, nor
|
|
14
|
+
* hand-format the `attribute:value` string the `bySpec` route splits on its
|
|
15
|
+
* first `:`.
|
|
16
|
+
*/
|
|
17
|
+
import { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, } from "./spec-types.js";
|
|
18
|
+
/**
|
|
19
|
+
* A term-equality criterion for `search({ filters })` — the attribute must
|
|
20
|
+
* carry this canonical value.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // "same fire class as the seed"
|
|
24
|
+
* client.search({ similarTo: { variantId }, filters: [specTerm("fire_classifications", "astm_e84_class_a")] });
|
|
25
|
+
*/
|
|
26
|
+
export function specTerm(attributeId, term) {
|
|
27
|
+
return { attributeId, term };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A presence criterion for `search({ filters })` — the attribute must have ANY
|
|
31
|
+
* accepted value.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // "…and it must have an EPD on record"
|
|
35
|
+
* client.search({ similarTo: { variantId }, filters: [hasSpec("environmental_product_declaration")] });
|
|
36
|
+
*/
|
|
37
|
+
export function hasSpec(attributeId) {
|
|
38
|
+
return { attributeId, hasValue: true };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A certification-programme criterion for `search({ filters })`, pinned to the
|
|
42
|
+
* virtual attribute so a programme matches wherever it landed at ingest
|
|
43
|
+
* (compliance / sustainability / material-health / VOC buckets).
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // "find a GREENGUARD-certified alternative"
|
|
47
|
+
* client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
|
|
48
|
+
*/
|
|
49
|
+
export function certificationProgramme(programme) {
|
|
50
|
+
return { attributeId: CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, term: programme };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* An `attribute:value` criterion string for `bySpec`. The `bySpec` route splits
|
|
54
|
+
* on the FIRST `:`, so a value may itself contain colons; the attribute id may
|
|
55
|
+
* not.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
|
|
59
|
+
*/
|
|
60
|
+
export function specCriterion(attributeId, value) {
|
|
61
|
+
return `${attributeId}:${value}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
65
|
+
* certification PROGRAMME across all four certification buckets.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // every brand's GREENGUARD-Gold materials
|
|
69
|
+
* client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
|
|
70
|
+
*/
|
|
71
|
+
export function certificationProgrammeCriterion(programme) {
|
|
72
|
+
return specCriterion(CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, programme);
|
|
73
|
+
}
|
package/sse.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, dependency-free Server-Sent Events reader — the primitive behind
|
|
3
|
+
* {@link MaterialGraphClient.streamRunEvents}. Hand-rolled (no `eventsource`,
|
|
4
|
+
* no `undici`) so the SDK stays zero-runtime-dep and works unchanged inside
|
|
5
|
+
* Node route handlers, where `fetch` yields a byte `ReadableStream`.
|
|
6
|
+
*
|
|
7
|
+
* The parser follows the WHATWG event-stream tokenizer closely enough for the
|
|
8
|
+
* `/api/v1/runs/{runId}/events` contract: `\n`, `\r\n`, and `\r` line endings,
|
|
9
|
+
* multi-line `data:` accumulation, comment (`:`-prefixed) lines, and a single
|
|
10
|
+
* optional leading space after the field colon. Frames split across network
|
|
11
|
+
* chunks are held in an internal buffer until their terminating blank line.
|
|
12
|
+
*/
|
|
13
|
+
/** One dispatched SSE frame. `data` is the joined data lines (no trailing newline). */
|
|
14
|
+
export interface SseFrame {
|
|
15
|
+
id?: string;
|
|
16
|
+
event?: string;
|
|
17
|
+
data: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A push-based SSE line assembler. Feed it decoded text with {@link push}; it
|
|
21
|
+
* returns any frames completed by that text. Call {@link flush} once the stream
|
|
22
|
+
* ends to dispatch a final frame that had data but no terminating blank line.
|
|
23
|
+
*/
|
|
24
|
+
export declare class SseDecoder {
|
|
25
|
+
#private;
|
|
26
|
+
push(text: string): SseFrame[];
|
|
27
|
+
/** Dispatch a trailing frame that had fields but no closing blank line. */
|
|
28
|
+
flush(): SseFrame[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
|
|
32
|
+
* or `signal` aborts (mid-read abort rejections are swallowed once the signal
|
|
33
|
+
* is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readSseFrames(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<SseFrame, void, unknown>;
|
package/sse.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, dependency-free Server-Sent Events reader — the primitive behind
|
|
3
|
+
* {@link MaterialGraphClient.streamRunEvents}. Hand-rolled (no `eventsource`,
|
|
4
|
+
* no `undici`) so the SDK stays zero-runtime-dep and works unchanged inside
|
|
5
|
+
* Node route handlers, where `fetch` yields a byte `ReadableStream`.
|
|
6
|
+
*
|
|
7
|
+
* The parser follows the WHATWG event-stream tokenizer closely enough for the
|
|
8
|
+
* `/api/v1/runs/{runId}/events` contract: `\n`, `\r\n`, and `\r` line endings,
|
|
9
|
+
* multi-line `data:` accumulation, comment (`:`-prefixed) lines, and a single
|
|
10
|
+
* optional leading space after the field colon. Frames split across network
|
|
11
|
+
* chunks are held in an internal buffer until their terminating blank line.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* A push-based SSE line assembler. Feed it decoded text with {@link push}; it
|
|
15
|
+
* returns any frames completed by that text. Call {@link flush} once the stream
|
|
16
|
+
* ends to dispatch a final frame that had data but no terminating blank line.
|
|
17
|
+
*/
|
|
18
|
+
export class SseDecoder {
|
|
19
|
+
#buffer = "";
|
|
20
|
+
#dataLines = [];
|
|
21
|
+
#id;
|
|
22
|
+
#event;
|
|
23
|
+
#hasFields = false;
|
|
24
|
+
push(text) {
|
|
25
|
+
this.#buffer += text;
|
|
26
|
+
const frames = [];
|
|
27
|
+
for (;;) {
|
|
28
|
+
const lineEnd = this.#nextLineEnd();
|
|
29
|
+
if (lineEnd === null)
|
|
30
|
+
break;
|
|
31
|
+
const line = this.#buffer.slice(0, lineEnd.index);
|
|
32
|
+
this.#buffer = this.#buffer.slice(lineEnd.nextIndex);
|
|
33
|
+
const frame = this.#consumeLine(line);
|
|
34
|
+
if (frame)
|
|
35
|
+
frames.push(frame);
|
|
36
|
+
}
|
|
37
|
+
return frames;
|
|
38
|
+
}
|
|
39
|
+
/** Dispatch a trailing frame that had fields but no closing blank line. */
|
|
40
|
+
flush() {
|
|
41
|
+
if (this.#buffer.length > 0) {
|
|
42
|
+
// At end of stream a deferred trailing CR is a real terminator, not data.
|
|
43
|
+
const line = this.#buffer.endsWith("\r") ? this.#buffer.slice(0, -1) : this.#buffer;
|
|
44
|
+
this.#buffer = "";
|
|
45
|
+
const frame = this.#consumeLine(line);
|
|
46
|
+
if (frame)
|
|
47
|
+
return [frame];
|
|
48
|
+
}
|
|
49
|
+
const frame = this.#dispatch();
|
|
50
|
+
return frame ? [frame] : [];
|
|
51
|
+
}
|
|
52
|
+
/** Locate the next line terminator, deferring a lone trailing `\r` (may be `\r\n`). */
|
|
53
|
+
#nextLineEnd() {
|
|
54
|
+
for (let i = 0; i < this.#buffer.length; i += 1) {
|
|
55
|
+
const char = this.#buffer[i];
|
|
56
|
+
if (char === "\n")
|
|
57
|
+
return { index: i, nextIndex: i + 1 };
|
|
58
|
+
if (char === "\r") {
|
|
59
|
+
if (i === this.#buffer.length - 1)
|
|
60
|
+
return null;
|
|
61
|
+
return this.#buffer[i + 1] === "\n"
|
|
62
|
+
? { index: i, nextIndex: i + 2 }
|
|
63
|
+
: { index: i, nextIndex: i + 1 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
#consumeLine(line) {
|
|
69
|
+
if (line.length === 0)
|
|
70
|
+
return this.#dispatch();
|
|
71
|
+
if (line.startsWith(":"))
|
|
72
|
+
return undefined;
|
|
73
|
+
const colon = line.indexOf(":");
|
|
74
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
75
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
76
|
+
if (value.startsWith(" "))
|
|
77
|
+
value = value.slice(1);
|
|
78
|
+
if (field === "data") {
|
|
79
|
+
this.#dataLines.push(value);
|
|
80
|
+
this.#hasFields = true;
|
|
81
|
+
}
|
|
82
|
+
else if (field === "event") {
|
|
83
|
+
this.#event = value;
|
|
84
|
+
this.#hasFields = true;
|
|
85
|
+
}
|
|
86
|
+
else if (field === "id") {
|
|
87
|
+
this.#id = value;
|
|
88
|
+
this.#hasFields = true;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
#dispatch() {
|
|
93
|
+
if (!this.#hasFields)
|
|
94
|
+
return undefined;
|
|
95
|
+
const frame = { data: this.#dataLines.join("\n") };
|
|
96
|
+
if (this.#event !== undefined)
|
|
97
|
+
frame.event = this.#event;
|
|
98
|
+
if (this.#id !== undefined)
|
|
99
|
+
frame.id = this.#id;
|
|
100
|
+
this.#dataLines = [];
|
|
101
|
+
this.#event = undefined;
|
|
102
|
+
this.#id = undefined;
|
|
103
|
+
this.#hasFields = false;
|
|
104
|
+
return frame;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
|
|
109
|
+
* or `signal` aborts (mid-read abort rejections are swallowed once the signal
|
|
110
|
+
* is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
|
|
111
|
+
*/
|
|
112
|
+
export async function* readSseFrames(body, signal) {
|
|
113
|
+
const reader = body.getReader();
|
|
114
|
+
const decoder = new TextDecoder();
|
|
115
|
+
const sse = new SseDecoder();
|
|
116
|
+
try {
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (signal?.aborted)
|
|
119
|
+
return;
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await reader.read();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (signal?.aborted)
|
|
126
|
+
return;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (result.done) {
|
|
130
|
+
for (const frame of sse.push(decoder.decode()))
|
|
131
|
+
yield frame;
|
|
132
|
+
for (const frame of sse.flush())
|
|
133
|
+
yield frame;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const text = decoder.decode(result.value, { stream: true });
|
|
137
|
+
for (const frame of sse.push(text)) {
|
|
138
|
+
if (signal?.aborted)
|
|
139
|
+
return;
|
|
140
|
+
yield frame;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
reader.releaseLock();
|
|
146
|
+
}
|
|
147
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -134,6 +134,32 @@ export interface SearchResultItem {
|
|
|
134
134
|
siblingCount?: number;
|
|
135
135
|
visualMeanings?: VisualMeaning[];
|
|
136
136
|
}
|
|
137
|
+
/** The lean `view:"card"` result item — identity + name + thumbnail + score. */
|
|
138
|
+
export interface SearchCardItem {
|
|
139
|
+
variantId: string;
|
|
140
|
+
productId: string | null;
|
|
141
|
+
brandSlug: string | null;
|
|
142
|
+
brand: string;
|
|
143
|
+
/** Product name (the full item's `product`). */
|
|
144
|
+
name: string;
|
|
145
|
+
variantName: string | null;
|
|
146
|
+
imageUrl: string | null;
|
|
147
|
+
score: number;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A feed total with its honesty basis. `exact` — the true count (≤ the cap of
|
|
151
|
+
* 1,000, or a facet-MV read). `lowerBound` — a floor (cap hit, ANN-pool bound,
|
|
152
|
+
* or a degraded count). Never a silent approximation.
|
|
153
|
+
*/
|
|
154
|
+
export interface CountedTotal {
|
|
155
|
+
value: number;
|
|
156
|
+
basis: OpenEnum<"exact" | "lowerBound">;
|
|
157
|
+
}
|
|
158
|
+
/** Feed counts — present under `count:true` or `collapse:"product"`. */
|
|
159
|
+
export interface SearchCounts {
|
|
160
|
+
variants: CountedTotal;
|
|
161
|
+
products?: CountedTotal;
|
|
162
|
+
}
|
|
137
163
|
/** The single candidate source a query resolved to. */
|
|
138
164
|
export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical">;
|
|
139
165
|
/** Rerank-term report (absent in colour mode — there colour IS the source). */
|
|
@@ -142,16 +168,22 @@ export interface SearchRerankReport {
|
|
|
142
168
|
colourWeight?: number;
|
|
143
169
|
colourLabel?: string;
|
|
144
170
|
}
|
|
145
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* The search response envelope. `view` discriminates the `results` union:
|
|
173
|
+
* `"full"` (default) carries {@link SearchResultItem}s, `"card"` the lean
|
|
174
|
+
* {@link SearchCardItem}s. Every other field is shared. `counts` is present
|
|
175
|
+
* under `count:true` or `collapse:"product"` (replacing the former
|
|
176
|
+
* `variantCount`/`productCount`).
|
|
177
|
+
*/
|
|
146
178
|
export interface SearchResponse {
|
|
179
|
+
view: OpenEnum<"full" | "card">;
|
|
147
180
|
results: SearchResultItem[];
|
|
148
181
|
suggestions: SearchSuggestion[];
|
|
149
182
|
mode: SearchMode;
|
|
150
183
|
maxScore: number;
|
|
151
184
|
cursor?: string;
|
|
152
185
|
rerank?: SearchRerankReport;
|
|
153
|
-
|
|
154
|
-
productCount?: number;
|
|
186
|
+
counts?: SearchCounts;
|
|
155
187
|
note?: string;
|
|
156
188
|
timings?: Record<string, number>;
|
|
157
189
|
/** Echo of the resolved query input (raw image bytes/vector are never echoed). */
|
|
@@ -159,6 +191,11 @@ export interface SearchResponse {
|
|
|
159
191
|
/** A `getVariantBrief` echo of the seed variant, present only in `variantId` seed mode. */
|
|
160
192
|
seed?: MaterialTile | Record<string, unknown>;
|
|
161
193
|
}
|
|
194
|
+
/** The `view:"card"` response form — the lean grid projection of {@link SearchResponse}. */
|
|
195
|
+
export interface SearchCardResponse extends Omit<SearchResponse, "view" | "results"> {
|
|
196
|
+
view: OpenEnum<"card">;
|
|
197
|
+
results: SearchCardItem[];
|
|
198
|
+
}
|
|
162
199
|
/** An inferred product-type category from the query image, with its boost. */
|
|
163
200
|
export interface MatchCategoryInference {
|
|
164
201
|
productTypeId: string;
|
|
@@ -437,6 +474,12 @@ export interface SearchInput {
|
|
|
437
474
|
cursor?: string;
|
|
438
475
|
explain?: boolean;
|
|
439
476
|
collapse?: OpenEnum<"none" | "product">;
|
|
477
|
+
/** Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools. */
|
|
478
|
+
count?: boolean;
|
|
479
|
+
/** Only return materials with at least one catalog image. Default false (full catalog). */
|
|
480
|
+
hasImages?: boolean;
|
|
481
|
+
/** Result projection — `"full"` (default) or the lean `"card"` grid form. */
|
|
482
|
+
view?: OpenEnum<"full" | "card">;
|
|
440
483
|
}
|
|
441
484
|
/** Query-string params for the GET `/search` form. */
|
|
442
485
|
export interface SearchGetParams {
|
|
@@ -450,6 +493,12 @@ export interface SearchGetParams {
|
|
|
450
493
|
limit?: number;
|
|
451
494
|
/** "0" or "1" — include per-signal `signals` on each result. */
|
|
452
495
|
explain?: OpenEnum<"0" | "1">;
|
|
496
|
+
/** "true" — opt in to a `counts.variants` feed total. */
|
|
497
|
+
count?: OpenEnum<"true">;
|
|
498
|
+
/** "true" — only return materials with at least one catalog image. */
|
|
499
|
+
hasImages?: OpenEnum<"true">;
|
|
500
|
+
/** Result projection — `"full"` (default) or the lean `"card"` grid form. */
|
|
501
|
+
view?: OpenEnum<"full" | "card">;
|
|
453
502
|
}
|
|
454
503
|
/** Params for `search/colours`. */
|
|
455
504
|
export interface ByColourParams {
|
|
@@ -559,6 +608,98 @@ export interface DemandResponse {
|
|
|
559
608
|
demand: MaterialDemandResult;
|
|
560
609
|
cospec: CoSpecifiedResult;
|
|
561
610
|
}
|
|
611
|
+
/** Which behavioural signal candidate demand is read from. */
|
|
612
|
+
export type DemandSignal = OpenEnum<"sampled" | "considered" | "specified">;
|
|
613
|
+
/**
|
|
614
|
+
* Body for `POST /api/v1/demand/batch` — enrich 1–48 already-selected candidate
|
|
615
|
+
* ids with their real specification demand, in input order and cardinality.
|
|
616
|
+
*/
|
|
617
|
+
export interface DemandBatchInput {
|
|
618
|
+
/** 1–48 canonical variant ids. Order AND cardinality are preserved — duplicates are echoed, never collapsed. */
|
|
619
|
+
variantIds: string[];
|
|
620
|
+
/** The behavioural signal to read demand from. Defaults to `sampled` (order co-occurrence) server-side. */
|
|
621
|
+
signal?: DemandSignal;
|
|
622
|
+
/** Optional declared context (e.g. `{ projectType: "hospitality" }`) — echoed for provenance, never used to rank or filter. */
|
|
623
|
+
context?: Record<string, string>;
|
|
624
|
+
}
|
|
625
|
+
/** Headline population behind the metric — this variant's own counts. */
|
|
626
|
+
export interface DemandBatchPopulation {
|
|
627
|
+
specCount: number;
|
|
628
|
+
projectCount: number;
|
|
629
|
+
customerCount: number;
|
|
630
|
+
}
|
|
631
|
+
/** The consideration→specification funnel (project-grain; `exclusionRate` is annotate-only). */
|
|
632
|
+
export interface DemandBatchFunnel {
|
|
633
|
+
consideredProjects: number;
|
|
634
|
+
specifiedProjects: number;
|
|
635
|
+
excludedCount: number;
|
|
636
|
+
exclusionRate: number;
|
|
637
|
+
}
|
|
638
|
+
/** Per-result guardrail status — present on `internal` (`mg:admin`) results only. */
|
|
639
|
+
export interface DemandBatchEvidence {
|
|
640
|
+
belowPrivacyFloor: boolean;
|
|
641
|
+
belowQualityFloor: boolean;
|
|
642
|
+
suppressedForPublic: boolean;
|
|
643
|
+
}
|
|
644
|
+
/** A variant that carries a usable demand signal. */
|
|
645
|
+
export interface DemandBatchAvailableResult {
|
|
646
|
+
variantId: string;
|
|
647
|
+
state: "available";
|
|
648
|
+
population: DemandBatchPopulation;
|
|
649
|
+
/** Exact per-quarter counts (sparse — zero-spec quarters are absent). */
|
|
650
|
+
intervals: DemandQuarter[];
|
|
651
|
+
momentum: DemandMomentum;
|
|
652
|
+
/** The funnel; null when the signal is not `sampled` or no `considered` row/session exists. */
|
|
653
|
+
funnel: DemandBatchFunnel | null;
|
|
654
|
+
/** Exact guardrail evidence for `internal` callers; null for public. */
|
|
655
|
+
evidence: DemandBatchEvidence | null;
|
|
656
|
+
}
|
|
657
|
+
/** A variant with a stats row that is below the public quality floor. */
|
|
658
|
+
export interface DemandBatchInsufficientResult {
|
|
659
|
+
variantId: string;
|
|
660
|
+
state: "insufficient_data";
|
|
661
|
+
}
|
|
662
|
+
/** A variant with no stats — indistinguishable from a privacy-suppressed one for public callers. */
|
|
663
|
+
export interface DemandBatchNoSignalResult {
|
|
664
|
+
variantId: string;
|
|
665
|
+
state: "no_signal";
|
|
666
|
+
}
|
|
667
|
+
/** A requested id that resolves to no real variant. */
|
|
668
|
+
export interface DemandBatchUnknownResult {
|
|
669
|
+
variantId: string;
|
|
670
|
+
state: "unknown_variant";
|
|
671
|
+
}
|
|
672
|
+
/** One discriminated result per input id, in input order and cardinality. */
|
|
673
|
+
export type DemandBatchResult = DemandBatchAvailableResult | DemandBatchInsufficientResult | DemandBatchNoSignalResult | DemandBatchUnknownResult;
|
|
674
|
+
/** The `POST /api/v1/demand/batch` envelope. */
|
|
675
|
+
export interface DemandBatchResponse {
|
|
676
|
+
/** The completed ingestion-session id these counts were mined in (the dataset generation). */
|
|
677
|
+
generationId: string;
|
|
678
|
+
metric: "specification_demand";
|
|
679
|
+
/** The metric universe — the window and signal the counts are scoped to. */
|
|
680
|
+
universe: {
|
|
681
|
+
window: "trailing_36mo";
|
|
682
|
+
signal: DemandSignal;
|
|
683
|
+
description: string;
|
|
684
|
+
};
|
|
685
|
+
/** The guardrail status under which `results` were projected. */
|
|
686
|
+
guardrail: {
|
|
687
|
+
audience: OpenEnum<"public" | "internal">;
|
|
688
|
+
minProjectCount: number;
|
|
689
|
+
minCustomerCount: number;
|
|
690
|
+
};
|
|
691
|
+
/** Outcome tallies over `results` — never a denominator for the metric. */
|
|
692
|
+
coverage: {
|
|
693
|
+
requested: number;
|
|
694
|
+
available: number;
|
|
695
|
+
insufficientData: number;
|
|
696
|
+
noSignal: number;
|
|
697
|
+
unknownVariant: number;
|
|
698
|
+
};
|
|
699
|
+
/** The echoed declared context, or null when none was supplied. */
|
|
700
|
+
context: Record<string, string> | null;
|
|
701
|
+
results: DemandBatchResult[];
|
|
702
|
+
}
|
|
562
703
|
export interface CoordinateSeed extends DemandSeed {
|
|
563
704
|
/** The input reference this seed resolved from. */
|
|
564
705
|
resolvedFrom: string;
|
|
@@ -742,7 +883,7 @@ export interface ExtractPaletteInput {
|
|
|
742
883
|
/** Options for a cacheable GET batch read (`materialsCached`). */
|
|
743
884
|
export interface MaterialsBatchGetOptions extends MaterialsBatchOptions {
|
|
744
885
|
}
|
|
745
|
-
/** Options for `brands/{
|
|
886
|
+
/** Options for `brands/{brandSlug}`. */
|
|
746
887
|
export interface BrandOptions {
|
|
747
888
|
/** Alternate fuzzy query; takes precedence over the path slug when present. */
|
|
748
889
|
q?: string;
|