@instruments/materialgraph 0.38.0 → 0.40.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/associations-client.js +12 -3
- package/by-spec-types.d.ts +1 -1
- package/catalogue-types.d.ts +36 -37
- package/categories-client.d.ts +2 -2
- package/client-response-schema.d.ts +3 -0
- package/client-response-schema.js +79 -0
- package/client.d.ts +9 -4
- package/client.js +46 -18
- package/colors-client.d.ts +5 -2
- package/colors-client.js +4 -0
- package/errors.d.ts +4 -4
- package/errors.js +13 -10
- package/find-variants-by-color-types.d.ts +10 -8
- package/find-variants-by-vector-types.d.ts +3 -0
- package/get-variant-types.d.ts +22 -1
- package/http-transport.d.ts +2 -1
- package/http-transport.js +12 -10
- package/index.d.ts +22 -21
- package/material-block-types.d.ts +7 -1
- package/material-resources-client.js +4 -1
- package/nearest-paints-types.d.ts +33 -0
- package/nearest-paints-types.js +1 -0
- package/operation-responses.d.ts +65 -0
- package/operation-responses.js +1 -0
- package/operations.d.ts +4 -12
- package/operations.generated.d.ts +11 -0
- package/operations.generated.js +189 -0
- package/operations.js +10 -178
- package/package.json +2 -2
- package/parse-operation-response.d.ts +5 -0
- package/parse-operation-response.js +202 -0
- package/response-schemas.generated.d.ts +1 -1
- package/response-schemas.generated.js +35 -32
- package/runs.d.ts +2 -1
- package/runs.js +45 -12
- package/samplize-catalogue-types.d.ts +8 -8
- package/samplize-colorvec.js +111 -80
- package/sse.d.ts +2 -1
- package/sse.js +7 -5
- package/types.d.ts +6 -6
package/runs.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export declare const getWorkflowRun: (transport: HttpTransport, workflowId: stri
|
|
|
15
15
|
* /api/v1/workflows/{workflowId}/runs/{runId}/events`, SSE). Iterates until the stream ends or
|
|
16
16
|
* `signal` aborts. One connection is bounded server-side (~25s or a terminal
|
|
17
17
|
* event); pass `after` to resume from the last observed sequence.
|
|
18
|
+
* @yields {RunEvent} Each usable Run event from the stream.
|
|
18
19
|
*/
|
|
19
|
-
export declare
|
|
20
|
+
export declare const streamRunEvents: (transport: HttpTransport, workflowId: string, runId: string, options?: StreamRunEventsOptions) => AsyncGenerator<RunEvent, void, unknown>;
|
|
20
21
|
/**
|
|
21
22
|
* Collect Run events until a terminal event, the deadline, or the stream ends —
|
|
22
23
|
* the bounded events-log pattern the console uses. A single pass over
|
package/runs.js
CHANGED
|
@@ -9,16 +9,52 @@ import { MaterialGraphError } from "./errors.js";
|
|
|
9
9
|
import { findSdkOperation, operationPath } from "./operations.js";
|
|
10
10
|
import { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
|
|
11
11
|
import { readSseFrames } from "./sse.js";
|
|
12
|
+
const hasSourceIdentity = (source) => typeof source === "object" &&
|
|
13
|
+
source !== null &&
|
|
14
|
+
"sourceId" in source &&
|
|
15
|
+
typeof source.sourceId === "string" &&
|
|
16
|
+
"role" in source &&
|
|
17
|
+
typeof source.role === "string" &&
|
|
18
|
+
"filename" in source &&
|
|
19
|
+
typeof source.filename === "string" &&
|
|
20
|
+
"mediaType" in source &&
|
|
21
|
+
typeof source.mediaType === "string";
|
|
22
|
+
const hasSourceMeasurements = (source) => typeof source === "object" &&
|
|
23
|
+
source !== null &&
|
|
24
|
+
"sha256" in source &&
|
|
25
|
+
typeof source.sha256 === "string" &&
|
|
26
|
+
"bytes" in source &&
|
|
27
|
+
typeof source.bytes === "number" &&
|
|
28
|
+
"pages" in source &&
|
|
29
|
+
(source.pages === null || typeof source.pages === "number");
|
|
30
|
+
const isSourceReceipt = (value) => typeof value === "object" &&
|
|
31
|
+
value !== null &&
|
|
32
|
+
"batchId" in value &&
|
|
33
|
+
typeof value.batchId === "string" &&
|
|
34
|
+
"sources" in value &&
|
|
35
|
+
Array.isArray(value.sources) &&
|
|
36
|
+
value.sources.every((source) => typeof source === "object" &&
|
|
37
|
+
source !== null &&
|
|
38
|
+
hasSourceIdentity(source) &&
|
|
39
|
+
hasSourceMeasurements(source));
|
|
40
|
+
const isRunEvent = (value) => typeof value === "object" &&
|
|
41
|
+
value !== null &&
|
|
42
|
+
"sequence" in value &&
|
|
43
|
+
typeof value.sequence === "number" &&
|
|
44
|
+
"type" in value &&
|
|
45
|
+
typeof value.type === "string";
|
|
12
46
|
const runOperation = (operationId) => {
|
|
13
47
|
const match = findSdkOperation(operationId);
|
|
14
48
|
if (!match) {
|
|
15
49
|
throw MaterialGraphError.unexpected(`Unknown SDK operation: ${operationId}`);
|
|
16
50
|
}
|
|
17
|
-
return match;
|
|
51
|
+
return { ...match, operationId };
|
|
18
52
|
};
|
|
19
53
|
/** Build a `Blob` from raw bytes, normalising to an ArrayBuffer-backed view. */
|
|
20
54
|
const bytesToBlob = (input) => {
|
|
21
|
-
const type = input.contentType
|
|
55
|
+
const type = input.contentType !== undefined && input.contentType !== ""
|
|
56
|
+
? { type: input.contentType }
|
|
57
|
+
: {};
|
|
22
58
|
if (input.data instanceof ArrayBuffer) {
|
|
23
59
|
return new Blob([input.data], type);
|
|
24
60
|
}
|
|
@@ -60,6 +96,9 @@ export const uploadSourceMaterial = async (transport, input, signal) => {
|
|
|
60
96
|
signal,
|
|
61
97
|
url,
|
|
62
98
|
});
|
|
99
|
+
if (!isSourceReceipt(parsed)) {
|
|
100
|
+
throw MaterialGraphError.unexpected("MaterialGraph source-material upload returned an invalid receipt");
|
|
101
|
+
}
|
|
63
102
|
return parsed;
|
|
64
103
|
};
|
|
65
104
|
/** Start an asynchronous Run of one workflow (`POST /api/v1/workflows/{workflowId}/runs`). */
|
|
@@ -85,22 +124,16 @@ const decodeRunEvent = (data) => {
|
|
|
85
124
|
catch (cause) {
|
|
86
125
|
throw MaterialGraphError.unexpected(`MaterialGraph run-event stream sent an unparseable data frame: ${String(cause)}`);
|
|
87
126
|
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const record = parsed;
|
|
92
|
-
if (typeof record.sequence !== "number" || typeof record.type !== "string") {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return parsed;
|
|
127
|
+
return isRunEvent(parsed) ? parsed : null;
|
|
96
128
|
};
|
|
97
129
|
/**
|
|
98
130
|
* Stream ordered Run events as an async iterator (`GET
|
|
99
131
|
* /api/v1/workflows/{workflowId}/runs/{runId}/events`, SSE). Iterates until the stream ends or
|
|
100
132
|
* `signal` aborts. One connection is bounded server-side (~25s or a terminal
|
|
101
133
|
* event); pass `after` to resume from the last observed sequence.
|
|
134
|
+
* @yields {RunEvent} Each usable Run event from the stream.
|
|
102
135
|
*/
|
|
103
|
-
export async function* streamRunEvents(transport, workflowId, runId, options = {}) {
|
|
136
|
+
export const streamRunEvents = async function* streamRunEvents(transport, workflowId, runId, options = {}) {
|
|
104
137
|
const body = await transport.openStream(runOperation("list_public_run_events"), {
|
|
105
138
|
pathParams: { runId, workflowId },
|
|
106
139
|
...(options.after === undefined
|
|
@@ -116,7 +149,7 @@ export async function* streamRunEvents(transport, workflowId, runId, options = {
|
|
|
116
149
|
yield event;
|
|
117
150
|
}
|
|
118
151
|
}
|
|
119
|
-
}
|
|
152
|
+
};
|
|
120
153
|
/**
|
|
121
154
|
* Collect Run events until a terminal event, the deadline, or the stream ends —
|
|
122
155
|
* the bounded events-log pattern the console uses. A single pass over
|
|
@@ -4,8 +4,6 @@ export type SamplizeCatalogueOptions = {
|
|
|
4
4
|
after?: string;
|
|
5
5
|
/** Exact canonical identities, at most 100. Unknown/ineligible IDs are omitted; no substitution. Combined filters intersect. */
|
|
6
6
|
variantIds?: string[];
|
|
7
|
-
/** Exact source collection handle; publisher remains Samplize. */
|
|
8
|
-
collectionHandle?: string;
|
|
9
7
|
/** Pin every subsequent page to the version returned by the first page. */
|
|
10
8
|
sourceVersion?: string;
|
|
11
9
|
};
|
|
@@ -20,9 +18,10 @@ export type SamplizeCatalogueProduct = {
|
|
|
20
18
|
};
|
|
21
19
|
hex: string;
|
|
22
20
|
hexSource: {
|
|
23
|
-
publisher:
|
|
24
|
-
url: string;
|
|
25
|
-
source: "
|
|
21
|
+
publisher: string;
|
|
22
|
+
url: string | null;
|
|
23
|
+
source: "resolved_variant";
|
|
24
|
+
valueSource: string | null;
|
|
26
25
|
};
|
|
27
26
|
lrv: number | null;
|
|
28
27
|
lrvSource: SamplizeLrvObservation | null;
|
|
@@ -36,8 +35,8 @@ export type SamplizeCatalogueProduct = {
|
|
|
36
35
|
a: number;
|
|
37
36
|
b: number;
|
|
38
37
|
};
|
|
39
|
-
sourceProductId: string;
|
|
40
|
-
url: string;
|
|
38
|
+
sourceProductId: string | null;
|
|
39
|
+
url: string | null;
|
|
41
40
|
sizeVariants: {
|
|
42
41
|
sourceVariantId: string;
|
|
43
42
|
sku: string | null;
|
|
@@ -56,6 +55,7 @@ export type SamplizeCataloguePage = {
|
|
|
56
55
|
source: Record<string, unknown>;
|
|
57
56
|
/** Number of eligible records matching the supplied filters. */
|
|
58
57
|
total: number;
|
|
58
|
+
unavailableFields: string[];
|
|
59
59
|
items: SamplizeCatalogueProduct[];
|
|
60
60
|
nextCursor: string | null;
|
|
61
61
|
/** True on the final page; completeness requires collecting total unique IDs. */
|
|
@@ -66,7 +66,7 @@ export type SamplizeHexObservation = {
|
|
|
66
66
|
publisher: string;
|
|
67
67
|
source: string;
|
|
68
68
|
sourceUrl: string | null;
|
|
69
|
-
|
|
69
|
+
valueSource: string;
|
|
70
70
|
observedAt: string | null;
|
|
71
71
|
differsFromSelected: boolean;
|
|
72
72
|
};
|
package/samplize-colorvec.js
CHANGED
|
@@ -2,7 +2,13 @@ import { EMBEDDING_VERSION, EMBEDDING_VECTOR_SIZE, embeddingCosineSimilarity, pa
|
|
|
2
2
|
export const SAMPLIZE_COLORVEC_RANKING_VERSION = "solid-colorvec-cosine-v2";
|
|
3
3
|
export const SAMPLIZE_COLORVEC_EMBEDDING_VERSION = EMBEDDING_VERSION;
|
|
4
4
|
const decodeVector = (encoded) => {
|
|
5
|
-
const bytes = Uint8Array.from(atob(encoded), (c) =>
|
|
5
|
+
const bytes = Uint8Array.from(atob(encoded), (c) => {
|
|
6
|
+
const codePoint = c.codePointAt(0);
|
|
7
|
+
if (codePoint === undefined) {
|
|
8
|
+
throw new Error("Missing encoded vector byte.");
|
|
9
|
+
}
|
|
10
|
+
return codePoint;
|
|
11
|
+
});
|
|
6
12
|
if (bytes.length !== EMBEDDING_VECTOR_SIZE * 4) {
|
|
7
13
|
throw new Error("Invalid color vector size.");
|
|
8
14
|
}
|
|
@@ -12,19 +18,23 @@ const decodeVector = (encoded) => {
|
|
|
12
18
|
const facets = ["brand", "family", "lightness", "intensity"];
|
|
13
19
|
const identityOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
14
20
|
export const samplizeSolidColorVector = (hex) => {
|
|
15
|
-
if (!/^#[0-9a-f]{6}$/
|
|
21
|
+
if (!/^#[0-9a-f]{6}$/iu.test(hex)) {
|
|
16
22
|
throw new Error("A six-digit published color is required.");
|
|
17
23
|
}
|
|
18
24
|
return paletteToEmbedding([{ hex, proportion: 1 }], { validateOklab: true });
|
|
19
25
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const reference = input.reference
|
|
26
|
+
const findReferencePaint = (paints, input) => {
|
|
27
|
+
const reference = input.reference !== undefined && input.reference !== ""
|
|
23
28
|
? paints.find((p) => p.variantId === input.reference)
|
|
24
29
|
: undefined;
|
|
25
|
-
if (input.reference && !reference) {
|
|
30
|
+
if (input.reference !== undefined && input.reference !== "" && !reference) {
|
|
26
31
|
throw new Error("The reference is absent from this catalogue version.");
|
|
27
32
|
}
|
|
33
|
+
return reference;
|
|
34
|
+
};
|
|
35
|
+
/** Canonical constraints are shared by full-cache similarity and constrained text retrieval. */
|
|
36
|
+
export const selectSamplizeEligiblePaints = (paints, input) => {
|
|
37
|
+
const reference = findReferencePaint(paints, input);
|
|
28
38
|
const low = input.lightnessMin ?? 0;
|
|
29
39
|
const high = input.lightnessMax ?? 1;
|
|
30
40
|
const cool = input.temperatureMin ?? -0.4;
|
|
@@ -57,7 +67,10 @@ export const selectSamplizeEligiblePaints = (paints, input) => {
|
|
|
57
67
|
p.oklab.L <= high &&
|
|
58
68
|
p.oklab.b >= cool &&
|
|
59
69
|
p.oklab.b <= warm &&
|
|
60
|
-
facets.every((f) => f === omit ||
|
|
70
|
+
facets.every((f) => f === omit ||
|
|
71
|
+
input[f] === undefined ||
|
|
72
|
+
input[f].length === 0 ||
|
|
73
|
+
input[f].includes(p[f]));
|
|
61
74
|
const counts = {
|
|
62
75
|
brand: {},
|
|
63
76
|
family: {},
|
|
@@ -74,6 +87,91 @@ export const selectSamplizeEligiblePaints = (paints, input) => {
|
|
|
74
87
|
const eligible = candidates.filter((p) => matches(p));
|
|
75
88
|
return { counts, eligible, reference };
|
|
76
89
|
};
|
|
90
|
+
const searchPalette = (input, reference) => {
|
|
91
|
+
const palette = (input.palette !== undefined && input.palette.length > 0
|
|
92
|
+
? input.palette
|
|
93
|
+
: [{ hex: input.hex ?? reference?.hex ?? "", proportion: 1 }])
|
|
94
|
+
.map((p) => ({ ...p, hex: p.hex.toLowerCase() }))
|
|
95
|
+
.toSorted((a, b) => b.proportion - a.proportion || identityOrder(a.hex, b.hex));
|
|
96
|
+
if (palette.length > 12 ||
|
|
97
|
+
palette.some((p) => !Number.isFinite(p.proportion) || p.proportion <= 0 || p.proportion > 1)) {
|
|
98
|
+
throw new Error("Supply up to twelve positive image color shares.");
|
|
99
|
+
}
|
|
100
|
+
return palette;
|
|
101
|
+
};
|
|
102
|
+
const searchSamplizePaints = (cache, paints, input, vectorFor) => {
|
|
103
|
+
const limit = input.limit ?? 48;
|
|
104
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
105
|
+
throw new Error("Choose between 1 and 100 suggestions.");
|
|
106
|
+
}
|
|
107
|
+
const { reference, eligible, counts } = selectSamplizeEligiblePaints(paints, input);
|
|
108
|
+
const palette = searchPalette(input, reference);
|
|
109
|
+
const total = palette.reduce((sum, p) => sum + p.proportion, 0);
|
|
110
|
+
const buckets = palette.map((p, position) => {
|
|
111
|
+
const bucketPaints = [];
|
|
112
|
+
return {
|
|
113
|
+
...p,
|
|
114
|
+
paints: bucketPaints,
|
|
115
|
+
position,
|
|
116
|
+
quota: Math.floor((limit * p.proportion) / total),
|
|
117
|
+
remainder: ((limit * p.proportion) / total) % 1,
|
|
118
|
+
vector: vectorFor(p.hex),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
let remaining = limit - buckets.reduce((sum, p) => sum + p.quota, 0);
|
|
122
|
+
for (const p of [...buckets].toSorted((a, b) => b.remainder - a.remainder || a.position - b.position)) {
|
|
123
|
+
if (remaining-- > 0) {
|
|
124
|
+
p.quota++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const paint of eligible) {
|
|
128
|
+
let best = buckets[0];
|
|
129
|
+
if (best === undefined) {
|
|
130
|
+
throw new Error("Missing first palette bucket.");
|
|
131
|
+
}
|
|
132
|
+
let score = embeddingCosineSimilarity(paint.vector, best.vector);
|
|
133
|
+
for (let i = 1; i < buckets.length; i++) {
|
|
134
|
+
const candidate = buckets[i];
|
|
135
|
+
if (candidate === undefined) {
|
|
136
|
+
throw new Error("Missing palette bucket.");
|
|
137
|
+
}
|
|
138
|
+
const next = embeddingCosineSimilarity(paint.vector, candidate.vector);
|
|
139
|
+
if (next > score) {
|
|
140
|
+
best = candidate;
|
|
141
|
+
score = next;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
best.paints.push({ id: paint.variantId, score });
|
|
145
|
+
}
|
|
146
|
+
const groups = buckets
|
|
147
|
+
.toSorted((a, b) => b.proportion - a.proportion || a.position - b.position)
|
|
148
|
+
.map((p) => {
|
|
149
|
+
const ids = p.paints
|
|
150
|
+
.toSorted((a, b) => b.score - a.score || identityOrder(a.id, b.id))
|
|
151
|
+
.slice(0, p.quota)
|
|
152
|
+
.map((paint) => paint.id);
|
|
153
|
+
return {
|
|
154
|
+
hex: p.hex,
|
|
155
|
+
ids,
|
|
156
|
+
proportion: p.proportion,
|
|
157
|
+
quota: p.quota,
|
|
158
|
+
shortfall: p.quota - ids.length,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const ids = groups.flatMap((p) => p.ids);
|
|
162
|
+
return {
|
|
163
|
+
countBasis: "complete-catalogue",
|
|
164
|
+
counts,
|
|
165
|
+
dictionaryVersion: cache.dictionaryVersion,
|
|
166
|
+
eligibleCount: eligible.length,
|
|
167
|
+
embeddingVersion: cache.embeddingVersion,
|
|
168
|
+
groups,
|
|
169
|
+
ids,
|
|
170
|
+
limited: eligible.length > ids.length,
|
|
171
|
+
rankingVersion: cache.rankingVersion,
|
|
172
|
+
sourceVersion: cache.sourceVersion,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
77
175
|
/** Official portable MG ranking over its complete versioned sampling-channel projection. */
|
|
78
176
|
export const createSamplizeColorVecIndex = (cache) => {
|
|
79
177
|
if (cache.embeddingVersion !== EMBEDDING_VERSION ||
|
|
@@ -106,7 +204,11 @@ export const createSamplizeColorVecIndex = (cache) => {
|
|
|
106
204
|
if (!value) {
|
|
107
205
|
value = samplizeSolidColorVector(hex);
|
|
108
206
|
if (vectors.size >= 512) {
|
|
109
|
-
vectors.
|
|
207
|
+
const oldest = vectors.keys().next().value;
|
|
208
|
+
if (oldest === undefined) {
|
|
209
|
+
throw new Error("Missing oldest cached color vector.");
|
|
210
|
+
}
|
|
211
|
+
vectors.delete(oldest);
|
|
110
212
|
}
|
|
111
213
|
vectors.set(key, value);
|
|
112
214
|
}
|
|
@@ -114,78 +216,7 @@ export const createSamplizeColorVecIndex = (cache) => {
|
|
|
114
216
|
};
|
|
115
217
|
return {
|
|
116
218
|
search(input) {
|
|
117
|
-
|
|
118
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
119
|
-
throw new Error("Choose between 1 and 100 suggestions.");
|
|
120
|
-
}
|
|
121
|
-
const { reference, eligible, counts } = selectSamplizeEligiblePaints(paints, input);
|
|
122
|
-
const palette = (input.palette?.length
|
|
123
|
-
? input.palette
|
|
124
|
-
: [{ hex: input.hex ?? reference?.hex ?? "", proportion: 1 }])
|
|
125
|
-
.map((p) => ({ ...p, hex: p.hex.toLowerCase() }))
|
|
126
|
-
.toSorted((a, b) => b.proportion - a.proportion || identityOrder(a.hex, b.hex));
|
|
127
|
-
if (palette.length > 12 ||
|
|
128
|
-
palette.some((p) => !Number.isFinite(p.proportion) ||
|
|
129
|
-
p.proportion <= 0 ||
|
|
130
|
-
p.proportion > 1)) {
|
|
131
|
-
throw new Error("Supply up to twelve positive image color shares.");
|
|
132
|
-
}
|
|
133
|
-
const total = palette.reduce((sum, p) => sum + p.proportion, 0);
|
|
134
|
-
const buckets = palette.map((p, position) => ({
|
|
135
|
-
...p,
|
|
136
|
-
paints: [],
|
|
137
|
-
position,
|
|
138
|
-
quota: Math.floor((limit * p.proportion) / total),
|
|
139
|
-
remainder: ((limit * p.proportion) / total) % 1,
|
|
140
|
-
vector: vectorFor(p.hex),
|
|
141
|
-
}));
|
|
142
|
-
let remaining = limit - buckets.reduce((sum, p) => sum + p.quota, 0);
|
|
143
|
-
for (const p of [...buckets].toSorted((a, b) => b.remainder - a.remainder || a.position - b.position)) {
|
|
144
|
-
if (remaining-- > 0) {
|
|
145
|
-
p.quota++;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
for (const paint of eligible) {
|
|
149
|
-
let best = buckets[0];
|
|
150
|
-
let score = embeddingCosineSimilarity(paint.vector, best.vector);
|
|
151
|
-
for (let i = 1; i < buckets.length; i++) {
|
|
152
|
-
const candidate = buckets[i];
|
|
153
|
-
const next = embeddingCosineSimilarity(paint.vector, candidate.vector);
|
|
154
|
-
if (next > score) {
|
|
155
|
-
best = candidate;
|
|
156
|
-
score = next;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
best.paints.push({ id: paint.variantId, score });
|
|
160
|
-
}
|
|
161
|
-
const groups = buckets
|
|
162
|
-
.toSorted((a, b) => b.proportion - a.proportion || a.position - b.position)
|
|
163
|
-
.map((p) => {
|
|
164
|
-
const ids = p.paints
|
|
165
|
-
.toSorted((a, b) => b.score - a.score || identityOrder(a.id, b.id))
|
|
166
|
-
.slice(0, p.quota)
|
|
167
|
-
.map((paint) => paint.id);
|
|
168
|
-
return {
|
|
169
|
-
hex: p.hex,
|
|
170
|
-
ids,
|
|
171
|
-
proportion: p.proportion,
|
|
172
|
-
quota: p.quota,
|
|
173
|
-
shortfall: p.quota - ids.length,
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
const ids = groups.flatMap((p) => p.ids);
|
|
177
|
-
return {
|
|
178
|
-
countBasis: "complete-catalogue",
|
|
179
|
-
counts,
|
|
180
|
-
dictionaryVersion: cache.dictionaryVersion,
|
|
181
|
-
eligibleCount: eligible.length,
|
|
182
|
-
embeddingVersion: cache.embeddingVersion,
|
|
183
|
-
groups,
|
|
184
|
-
ids,
|
|
185
|
-
limited: eligible.length > ids.length,
|
|
186
|
-
rankingVersion: cache.rankingVersion,
|
|
187
|
-
sourceVersion: cache.sourceVersion,
|
|
188
|
-
};
|
|
219
|
+
return searchSamplizePaints(cache, paints, input, vectorFor);
|
|
189
220
|
},
|
|
190
221
|
};
|
|
191
222
|
};
|
package/sse.d.ts
CHANGED
|
@@ -31,5 +31,6 @@ export declare class SseDecoder {
|
|
|
31
31
|
* Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
|
|
32
32
|
* or `signal` aborts (mid-read abort rejections are swallowed once the signal
|
|
33
33
|
* is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
|
|
34
|
+
* @yields {SseFrame} Each completed frame containing fields.
|
|
34
35
|
*/
|
|
35
|
-
export declare
|
|
36
|
+
export declare const readSseFrames: (body: ReadableStream<Uint8Array>, signal?: AbortSignal) => AsyncGenerator<SseFrame, void, unknown>;
|
package/sse.js
CHANGED
|
@@ -121,14 +121,16 @@ export class SseDecoder {
|
|
|
121
121
|
* Iterate the SSE frames of a byte stream. Stops cleanly when the stream ends
|
|
122
122
|
* or `signal` aborts (mid-read abort rejections are swallowed once the signal
|
|
123
123
|
* is set). Frames with no `data` field (comments, bare `id:`) are not yielded.
|
|
124
|
+
* @yields {SseFrame} Each completed frame containing fields.
|
|
124
125
|
*/
|
|
125
|
-
export async function* readSseFrames(body, signal) {
|
|
126
|
+
export const readSseFrames = async function* readSseFrames(body, signal) {
|
|
127
|
+
const isAborted = () => signal?.aborted === true;
|
|
126
128
|
const reader = body.getReader();
|
|
127
129
|
const decoder = new TextDecoder();
|
|
128
130
|
const sse = new SseDecoder();
|
|
129
131
|
try {
|
|
130
132
|
for (;;) {
|
|
131
|
-
if (
|
|
133
|
+
if (isAborted()) {
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
136
|
let result;
|
|
@@ -136,7 +138,7 @@ export async function* readSseFrames(body, signal) {
|
|
|
136
138
|
result = await reader.read();
|
|
137
139
|
}
|
|
138
140
|
catch (error) {
|
|
139
|
-
if (
|
|
141
|
+
if (isAborted()) {
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
144
|
throw error;
|
|
@@ -152,7 +154,7 @@ export async function* readSseFrames(body, signal) {
|
|
|
152
154
|
}
|
|
153
155
|
const text = decoder.decode(result.value, { stream: true });
|
|
154
156
|
for (const frame of sse.push(text)) {
|
|
155
|
-
if (
|
|
157
|
+
if (isAborted()) {
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
yield frame;
|
|
@@ -162,4 +164,4 @@ export async function* readSseFrames(body, signal) {
|
|
|
162
164
|
finally {
|
|
163
165
|
reader.releaseLock();
|
|
164
166
|
}
|
|
165
|
-
}
|
|
167
|
+
};
|
package/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { MaterialTile } from "./material-types.js";
|
|
|
10
10
|
* against the real Zod schemas so the mirror cannot silently drift.
|
|
11
11
|
* TOLERANT-PARSING CONTRACT: the public surface declares open response enums —
|
|
12
12
|
* "clients must tolerate unknown enum values and unknown fields". Every union
|
|
13
|
-
* that mirrors a server enum is therefore widened with `(string &
|
|
13
|
+
* that mirrors a server enum is therefore widened with `(string & Record<never, never>)` so a new
|
|
14
14
|
* server value still type-checks, and objects that may grow fields are read
|
|
15
15
|
* structurally (unknown keys pass through untouched — the client never strips).
|
|
16
16
|
*
|
|
@@ -21,12 +21,12 @@ import type { MaterialTile } from "./material-types.js";
|
|
|
21
21
|
* are canonical vocabulary you discover.
|
|
22
22
|
* 2. **Absent is not false.** An optional field that is missing means UNKNOWN,
|
|
23
23
|
* not "known to be absent in reality" — most dangerously for booleans.
|
|
24
|
-
* Values carry `confidence` and `
|
|
24
|
+
* Values carry `confidence` and `valueSource`; report them as claims.
|
|
25
25
|
* 3. **A page is not a corpus.** A list is this page. Follow the cursor for
|
|
26
26
|
* the rest, and never report a page size as a total.
|
|
27
27
|
*/
|
|
28
28
|
import type { SearchCatalogueSubset } from "./search-catalogue-subset-types.js";
|
|
29
|
-
export type OpenEnum<T extends string> = T | (string &
|
|
29
|
+
export type OpenEnum<T extends string> = T | (string & Record<never, never>);
|
|
30
30
|
export type { JsonValue } from "./json-value-types.js";
|
|
31
31
|
/** OKLab triple. */
|
|
32
32
|
export type OklabTriple = {
|
|
@@ -247,9 +247,9 @@ export type ListAttributesResult = {
|
|
|
247
247
|
hasNextPage: boolean;
|
|
248
248
|
};
|
|
249
249
|
};
|
|
250
|
-
export type
|
|
251
|
-
export type
|
|
252
|
-
export type
|
|
250
|
+
export type { ResolveValueCandidate, ValueResolution, } from "./catalog-discovery-types.js";
|
|
251
|
+
export type { ResolveColorOptions, ResolveColorCatalogueVariant, ResolveColorVariant, ResolveColorResponse, } from "./resolve-color-types.js";
|
|
252
|
+
export type { BySpecVerdictState, BySpecVerdictCell, BySpecVerdict, MaterialVerdict, BySpecUnresolvedCriterion, } from "./by-spec-types.js";
|
|
253
253
|
export type WhoamiResponse = {
|
|
254
254
|
principal: string;
|
|
255
255
|
scopes: string[];
|