@instruments/materialgraph 0.9.1 → 0.10.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/brands-client.d.ts +48 -0
- package/brands-client.js +45 -0
- package/by-colour-types.d.ts +49 -5
- package/client.d.ts +36 -49
- package/client.js +57 -54
- package/errors.js +24 -8
- package/fan-deck-types.d.ts +39 -0
- package/fan-deck-types.js +9 -0
- package/http-transport.js +24 -5
- package/index.d.ts +3 -2
- package/index.js +2 -2
- package/match-stream-types.d.ts +1 -1
- package/match-stream-types.js +16 -3
- package/match-stream.js +14 -4
- package/material-types.d.ts +1 -1
- package/operations.d.ts +1 -1
- package/operations.js +162 -26
- package/package.json +1 -1
- package/resolve-client.d.ts +22 -3
- package/resolve-client.js +31 -4
- package/resolve-types.d.ts +70 -1
- package/run-events.d.ts +1 -1
- package/run-events.js +4 -1
- package/runs.d.ts +1 -1
- package/runs.js +8 -4
- package/search-refinement-types.d.ts +11 -3
- package/search-result-validation.js +21 -4
- package/source-material-validation.js +3 -1
- package/spec-facet-types.d.ts +53 -0
- package/spec-facet-types.js +7 -0
- package/specifications.d.ts +26 -0
- package/specifications.js +32 -0
- package/sse.js +3 -1
- package/suggest-types.js +3 -1
- package/types.d.ts +42 -32
package/resolve-types.d.ts
CHANGED
|
@@ -27,8 +27,18 @@
|
|
|
27
27
|
* pair, for `mg.bySpec()` or `search({ filters })`.
|
|
28
28
|
* - `attribute_value` — a value-dictionary term, namespaced (`ms_sheen.matte`):
|
|
29
29
|
* the RIGHT half.
|
|
30
|
+
* - `jurisdiction` — a code jurisdiction id (`us-tx`), for `mg.codeAdvisory()`.
|
|
31
|
+
* - `occupancy_group` — an IBC occupancy group (`R-1`), the other axis the code
|
|
32
|
+
* endpoints are keyed on. BARE, not namespaced, even though the groups live in
|
|
33
|
+
* a value dictionary: `R-1` is the token those endpoints take.
|
|
34
|
+
*
|
|
35
|
+
* The two code axes are half a context, not a whole one. `/codes/advisory` also
|
|
36
|
+
* needs a `spaceType` (or a `roomType` to correlate one from) and a
|
|
37
|
+
* `sprinklered` flag — neither of which a phrase can state — and it validates
|
|
38
|
+
* the combination before it answers, so send them and read its verdict rather
|
|
39
|
+
* than assuming a resolved pair is usable.
|
|
30
40
|
*/
|
|
31
|
-
export type ResolutionKind = "brand" | "paint_identity" | "colour" | "category" | "attribute" | "attribute_value";
|
|
41
|
+
export type ResolutionKind = "brand" | "paint_identity" | "colour" | "category" | "attribute" | "attribute_value" | "jurisdiction" | "occupancy_group";
|
|
32
42
|
/**
|
|
33
43
|
* How the phrase matched, strongest first. `exact` — it IS the canonical token;
|
|
34
44
|
* `alias` — it IS a recorded alternative name for it; `lexical` — it resembles
|
|
@@ -68,6 +78,49 @@ export interface ResolvedMeaning {
|
|
|
68
78
|
ambiguity?: {
|
|
69
79
|
gap: number;
|
|
70
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* How much of the catalogue stands behind this reading — so you never have to
|
|
83
|
+
* make a second, differently-shaped call to find out that a confident answer
|
|
84
|
+
* is empty.
|
|
85
|
+
*
|
|
86
|
+
* ABSENT means coverage could not be determined for this reading; it is a
|
|
87
|
+
* statement about the MEASUREMENT, never about the catalogue. A measured
|
|
88
|
+
* nothing is `{ variants: 0 }`. Do not collapse the two: `attribute` and
|
|
89
|
+
* `attribute_value` readings (and `brand`, whose corpus weight comes free)
|
|
90
|
+
* carry it; the rest have no equivalently cheap count behind them and report
|
|
91
|
+
* nothing rather than a figure measured a different way.
|
|
92
|
+
*/
|
|
93
|
+
coverage?: MeaningCoverage;
|
|
94
|
+
}
|
|
95
|
+
/** How much of the catalogue stands behind one reading. */
|
|
96
|
+
export interface MeaningCoverage {
|
|
97
|
+
/**
|
|
98
|
+
* How many variants carry this reading, CORPUS-WIDE and across every brand —
|
|
99
|
+
* never scoped to the `category` you sent, even when you sent one. `0` is a
|
|
100
|
+
* measured answer, not missing data: MaterialGraph knows the word and the
|
|
101
|
+
* catalogue holds nothing under it, which is the case worth catching before
|
|
102
|
+
* you spend the token on a filter.
|
|
103
|
+
*
|
|
104
|
+
* For an `attribute` reading over an ARRAY-valued attribute a variant is
|
|
105
|
+
* counted once per token it carries, so the figure is an upper bound on
|
|
106
|
+
* distinct variants there. It is exact for scalar attributes, exact for every
|
|
107
|
+
* `attribute_value`, and the zero is exact in every case.
|
|
108
|
+
*/
|
|
109
|
+
variants: number;
|
|
110
|
+
/**
|
|
111
|
+
* Whether the `category` you sent holds ANY accepted value for the attribute
|
|
112
|
+
* behind this reading. Present only when you sent a category AND the answer
|
|
113
|
+
* could be determined — its absence is a statement about the measurement, not
|
|
114
|
+
* a `false`.
|
|
115
|
+
*
|
|
116
|
+
* `false` means no criterion built on this reading can be satisfied inside
|
|
117
|
+
* that category at any rung of retrieval's relaxation ladder, however large
|
|
118
|
+
* `variants` is. Read this rather than inferring reachability from the count:
|
|
119
|
+
* an attribute with forty thousand values across the catalogue can hold none
|
|
120
|
+
* at all in Wall Finishes. The probe is ATTRIBUTE-level, so `true` says the
|
|
121
|
+
* category holds SOME value for the attribute, not that it holds this one.
|
|
122
|
+
*/
|
|
123
|
+
inCategory?: boolean;
|
|
71
124
|
}
|
|
72
125
|
/** What one phrase resolved to — including, explicitly, nothing. */
|
|
73
126
|
export interface PhraseResolution {
|
|
@@ -95,6 +148,22 @@ export interface PhraseResolution {
|
|
|
95
148
|
*/
|
|
96
149
|
unresolved: boolean;
|
|
97
150
|
}
|
|
151
|
+
/** Optional scoping for a resolve call. */
|
|
152
|
+
export interface ResolvePhrasesOptions {
|
|
153
|
+
/**
|
|
154
|
+
* The product category you are grounding these words FOR — a slot, not a
|
|
155
|
+
* filter. It adds {@link MeaningCoverage.inCategory} to every `attribute` and
|
|
156
|
+
* `attribute_value` reading. Omit for corpus-wide only.
|
|
157
|
+
*/
|
|
158
|
+
category?: string;
|
|
159
|
+
signal?: AbortSignal;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* What `mg.resolve()` accepts as its second argument. A bare `AbortSignal` is
|
|
163
|
+
* still accepted, exactly as before {@link ResolvePhrasesOptions} existed, so no
|
|
164
|
+
* existing call site had to move.
|
|
165
|
+
*/
|
|
166
|
+
export type ResolvePhrasesScope = AbortSignal | ResolvePhrasesOptions;
|
|
98
167
|
/** The resolve response: one entry per phrase, in the order you sent them. */
|
|
99
168
|
export interface ResolveResponse {
|
|
100
169
|
/** One per phrase, in request order — never reordered, never deduplicated. */
|
package/run-events.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunEvent } from "./run-types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Typed payloads for the composition Run event family — the friction fix flagged
|
|
3
4
|
* by the MG-443 walk. These narrow the loose {@link RunEvent.payloadJson} so
|
|
@@ -13,7 +14,6 @@
|
|
|
13
14
|
* as standalone TS types (no cross-package import; the SDK is publishable).
|
|
14
15
|
*/
|
|
15
16
|
import type { OpenEnum } from "./types.js";
|
|
16
|
-
import type { RunEvent } from "./run-types.js";
|
|
17
17
|
export declare const COMPOSITION_PHASE_EVENT_TYPES: readonly ["composition.phase", "material-box.phase"];
|
|
18
18
|
export declare const COMPOSITION_PROGRAMME_EVENT_TYPES: readonly ["composition.programme", "material-box.brief"];
|
|
19
19
|
export declare const COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES: readonly ["composition.review-criteria", "material-box.review-criteria"];
|
package/run-events.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// ── Wire event-type strings (both generations) ───────────────────────────────
|
|
2
|
-
export const COMPOSITION_PHASE_EVENT_TYPES = [
|
|
2
|
+
export const COMPOSITION_PHASE_EVENT_TYPES = [
|
|
3
|
+
"composition.phase",
|
|
4
|
+
"material-box.phase",
|
|
5
|
+
];
|
|
3
6
|
export const COMPOSITION_PROGRAMME_EVENT_TYPES = [
|
|
4
7
|
"composition.programme",
|
|
5
8
|
"material-box.brief",
|
package/runs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpTransport } from "./http-transport.js";
|
|
2
|
-
import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
|
|
3
2
|
import { type CapabilityManifestResponse, type CreatePublicRunRequest, type CreatePublicRunResponse, type DrainRunEventsOptions, type ListRunsOptions, type PublicRunListResponse, type PublicRunResponse, type ResumeRunOptions, type RunEvent, type SourceMaterialBatchDetail, type SourceMaterialBatchReceipt, type StreamRunEventsOptions, type UploadSourceMaterialInput } from "./run-types.js";
|
|
3
|
+
import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Upload one atomic source-material batch (`POST /api/v1/source-material`).
|
|
6
6
|
* Accepts the pile-of-files shape (`{ files }` → repeatable `files` field) or
|
package/runs.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { MaterialGraphError } from "./errors.js";
|
|
9
9
|
import { HttpTransport } from "./http-transport.js";
|
|
10
|
-
import { findSdkOperation, operationPath } from "./operations.js";
|
|
10
|
+
import { findSdkOperation, operationPath, } from "./operations.js";
|
|
11
|
+
import { TERMINAL_RUN_EVENT_TYPES, } from "./run-types.js";
|
|
11
12
|
import { checkSourceMaterialBatchDetail } from "./source-material-validation.js";
|
|
12
13
|
import { readSseFrames } from "./sse.js";
|
|
13
|
-
import { TERMINAL_RUN_EVENT_TYPES, } from "./run-types.js";
|
|
14
14
|
function runOperation(operationId) {
|
|
15
15
|
const match = findSdkOperation(operationId);
|
|
16
16
|
if (!match)
|
|
@@ -224,7 +224,9 @@ function decodeRunEvent(data) {
|
|
|
224
224
|
export async function* streamRunEvents(transport, runId, options = {}) {
|
|
225
225
|
const body = await transport.openStream(runOperation("list_public_run_events"), {
|
|
226
226
|
pathParams: { runId },
|
|
227
|
-
...(options.after === undefined
|
|
227
|
+
...(options.after === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { query: { after: options.after } }),
|
|
228
230
|
...(options.signal ? { signal: options.signal } : {}),
|
|
229
231
|
accept: "text/event-stream",
|
|
230
232
|
subject: "run-event stream",
|
|
@@ -265,7 +267,9 @@ export async function drainRunEvents(transport, runId, options) {
|
|
|
265
267
|
}
|
|
266
268
|
}
|
|
267
269
|
catch (error) {
|
|
268
|
-
const causedByDeadline = controller.signal.aborted &&
|
|
270
|
+
const causedByDeadline = controller.signal.aborted &&
|
|
271
|
+
error instanceof MaterialGraphError &&
|
|
272
|
+
error.kind === "aborted";
|
|
269
273
|
if (!causedByDeadline)
|
|
270
274
|
throw error;
|
|
271
275
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Types for structured refinements returned by MaterialGraph search. */
|
|
2
2
|
import type { SearchInput } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* A hard canonical-attribute filter accepted by the search engine —
|
|
5
|
-
*
|
|
6
|
-
* (`{ attributeId,
|
|
4
|
+
* A hard canonical-attribute filter accepted by the search engine — a term match
|
|
5
|
+
* (`{ attributeId, term }`), a presence check (`{ attributeId, hasValue: true }`)
|
|
6
|
+
* or a NUMERIC RANGE (`{ attributeId, min, max }`, bounds inclusive, at least one
|
|
7
|
+
* stated). Applied as a restriction BEFORE ranking.
|
|
7
8
|
*
|
|
8
9
|
* The `attributeId` may be a real registry id (`"fire_classifications"`,
|
|
9
10
|
* `"environmental_product_declaration"`) OR the virtual
|
|
@@ -52,6 +53,13 @@ export type SearchFilter = {
|
|
|
52
53
|
attributeId: string;
|
|
53
54
|
hasValue: true;
|
|
54
55
|
trust?: SearchFilterTrust;
|
|
56
|
+
} | {
|
|
57
|
+
attributeId: string;
|
|
58
|
+
/** Inclusive lower bound. Omit for a ceiling-only range. */
|
|
59
|
+
min?: number;
|
|
60
|
+
/** Inclusive upper bound. Omit for a floor-only range. */
|
|
61
|
+
max?: number;
|
|
62
|
+
trust?: SearchFilterTrust;
|
|
55
63
|
};
|
|
56
64
|
/** One structured refinement returned with a search response. */
|
|
57
65
|
export interface SearchSuggestion {
|
|
@@ -12,17 +12,33 @@ export const VERDICT_STATES = [
|
|
|
12
12
|
/** `attributeVerdictSchema.strength`. */
|
|
13
13
|
export const VERDICT_STRENGTHS = ["required", "preferred"];
|
|
14
14
|
/** `verdictBaselineSchema` discriminant — the four arms of what a cell was judged against. */
|
|
15
|
-
export const VERDICT_BASELINE_KINDS = [
|
|
15
|
+
export const VERDICT_BASELINE_KINDS = [
|
|
16
|
+
"term",
|
|
17
|
+
"presence",
|
|
18
|
+
"seed",
|
|
19
|
+
"image",
|
|
20
|
+
];
|
|
16
21
|
/** The `image` baseline arm's own `source` enum. */
|
|
17
22
|
export const VERDICT_IMAGE_SOURCES = ["variant", "vector"];
|
|
18
23
|
/** `verdictDeviationSchema.direction`. */
|
|
19
|
-
export const VERDICT_DEVIATION_DIRECTIONS = [
|
|
24
|
+
export const VERDICT_DEVIATION_DIRECTIONS = [
|
|
25
|
+
"above",
|
|
26
|
+
"below",
|
|
27
|
+
"other",
|
|
28
|
+
];
|
|
20
29
|
/** `verdictTrustGapSchema` — why a real value did not clear the trust bar. */
|
|
21
|
-
export const VERDICT_TRUST_GAPS = [
|
|
30
|
+
export const VERDICT_TRUST_GAPS = [
|
|
31
|
+
"below_confidence",
|
|
32
|
+
"basis_unmet",
|
|
33
|
+
"proposed",
|
|
34
|
+
];
|
|
22
35
|
/** `verdictGradeSchema` — the rolled-up card grade. */
|
|
23
36
|
export const VERDICT_GRADES = ["meets_all", "close", "hard_no"];
|
|
24
37
|
/** `visualMeaningSchema.basis` — where a meaning came from. */
|
|
25
|
-
export const VISUAL_MEANING_BASES = [
|
|
38
|
+
export const VISUAL_MEANING_BASES = [
|
|
39
|
+
"canonical_attribute",
|
|
40
|
+
"image_colour_analysis",
|
|
41
|
+
];
|
|
26
42
|
// ── Leaf checks ──────────────────────────────────────────────────────────────
|
|
27
43
|
/** `palette[]` — `{ hex, proportion, name: string.nullable().optional() }`. */
|
|
28
44
|
function checkPaletteSwatch(value, what, fail) {
|
|
@@ -165,6 +181,7 @@ export function checkSearchResultItem(value, what, fail) {
|
|
|
165
181
|
checkSignals(record.signals, `${what}.signals`, fail);
|
|
166
182
|
requireOptionalBoolean(record.retired, `${what}.retired`, fail);
|
|
167
183
|
requireOptionalBoolean(record.belowFloor, `${what}.belowFloor`, fail);
|
|
184
|
+
requireOptionalBoolean(record.admittedOnColour, `${what}.admittedOnColour`, fail);
|
|
168
185
|
if (record.siblingCount !== undefined) {
|
|
169
186
|
requireIntegerAtLeast(record.siblingCount, `${what}.siblingCount`, fail, 1);
|
|
170
187
|
}
|
|
@@ -91,7 +91,9 @@ function checkFile(value, what, fail) {
|
|
|
91
91
|
// fields is a canonical change to a schema shared with the upload path, and
|
|
92
92
|
// belongs in its own ticket with this mirror following after.
|
|
93
93
|
bytes: requireSafeInteger(record.bytes, `${what}.bytes`, fail),
|
|
94
|
-
pages: record.pages === null
|
|
94
|
+
pages: record.pages === null
|
|
95
|
+
? null
|
|
96
|
+
: requireSafeInteger(record.pages, `${what}.pages`, fail),
|
|
95
97
|
url: requireString(record.url, `${what}.url`, fail),
|
|
96
98
|
analyses: analyses.map((analysis, index) => checkAnalysis(analysis, `${what}.analyses[${index}]`, fail)),
|
|
97
99
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface SpecFacetValue {
|
|
2
|
+
value: string;
|
|
3
|
+
variants: number;
|
|
4
|
+
brands: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A NUMERIC facet's observed span and the coverage behind it.
|
|
9
|
+
*
|
|
10
|
+
* A range, not buckets — no bucketing scheme has been decided, and one invented
|
|
11
|
+
* on the wire would be treated as canonical by everyone who read it. Both bounds
|
|
12
|
+
* are INCLUSIVE, matching `specRange`.
|
|
13
|
+
*
|
|
14
|
+
* READ `variants` BEFORE TRUSTING A FILTER. It is how many variants carry a
|
|
15
|
+
* value that parses as a number, i.e. how much of the corpus a range can reach;
|
|
16
|
+
* a facet that exists says the attribute is queryable, not that it is populated.
|
|
17
|
+
* `unparseableVariants` is the honesty companion: values recorded here that are
|
|
18
|
+
* not numbers, which a range EXCLUDES rather than reading as zero.
|
|
19
|
+
*/
|
|
20
|
+
export interface SpecFacetRange {
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
variants: number;
|
|
24
|
+
unparseableVariants: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SpecFacet {
|
|
27
|
+
attributeId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
isArray: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* How this facet is queried. `token` — pick a value from `values` and send it
|
|
32
|
+
* as `attribute:value`. `numeric` — `values` is EMPTY and `range` carries the
|
|
33
|
+
* span; send inclusive bounds via `specRangeCriterion` instead.
|
|
34
|
+
*/
|
|
35
|
+
kind: "token" | "numeric" | (string & {});
|
|
36
|
+
brands: number;
|
|
37
|
+
/** EMPTY for a `numeric` facet, which has no token vocabulary. */
|
|
38
|
+
values: SpecFacetValue[];
|
|
39
|
+
/** Present ONLY on a `numeric` facet. */
|
|
40
|
+
range?: SpecFacetRange;
|
|
41
|
+
description?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SpecFacetsListResponse {
|
|
44
|
+
mode: "facets";
|
|
45
|
+
count: number;
|
|
46
|
+
facets: SpecFacet[];
|
|
47
|
+
}
|
|
48
|
+
export interface SpecFacetsDistributionResponse {
|
|
49
|
+
mode: "distribution";
|
|
50
|
+
attributeId: string;
|
|
51
|
+
count: number;
|
|
52
|
+
values: SpecFacetValue[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ── Specification facets + attribute values ─────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Hand-maintained mirror of the `SpecificationFacet` OpenAPI schema (the source
|
|
4
|
+
// of truth on the wire); the `sdk-openapi-parity` test gates this against the
|
|
5
|
+
// generated spec. Split out of `types.ts` to keep that file under its growth
|
|
6
|
+
// baseline; re-exported from `types.ts` so importers see no change.
|
|
7
|
+
export {};
|
package/specifications.d.ts
CHANGED
|
@@ -57,6 +57,32 @@ export declare function certificationProgramme(programme: string): Certification
|
|
|
57
57
|
* client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
|
|
58
58
|
*/
|
|
59
59
|
export declare function specCriterion(attributeId: string, value: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* An `attribute:min..max` RANGE criterion string for `bySpec`, for a
|
|
62
|
+
* NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
|
|
63
|
+
* `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
|
|
64
|
+
*
|
|
65
|
+
* BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
|
|
66
|
+
* because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
|
|
67
|
+
* and because inclusive-both-ends means adjacent ranges overlap at the join
|
|
68
|
+
* rather than leaving a gap that hides products.
|
|
69
|
+
*
|
|
70
|
+
* At least one bound is REQUIRED: a range with neither is a presence check, and
|
|
71
|
+
* this throws rather than sending a criterion the server would have to refuse.
|
|
72
|
+
* Discover which attributes take a range — and the span they actually occupy —
|
|
73
|
+
* from `specificationFacets()`: those are the facets with `kind: "numeric"`.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // light paints, every brand at once
|
|
77
|
+
* client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
|
|
78
|
+
* @example
|
|
79
|
+
* // a mid-tone band
|
|
80
|
+
* specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
|
|
81
|
+
*/
|
|
82
|
+
export declare function specRangeCriterion(attributeId: string, bounds: {
|
|
83
|
+
min?: number;
|
|
84
|
+
max?: number;
|
|
85
|
+
}): string;
|
|
60
86
|
/**
|
|
61
87
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
62
88
|
* certification PROGRAMME across all four certification buckets.
|
package/specifications.js
CHANGED
|
@@ -65,6 +65,38 @@ export function certificationProgramme(programme) {
|
|
|
65
65
|
export function specCriterion(attributeId, value) {
|
|
66
66
|
return `${attributeId}:${value}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* An `attribute:min..max` RANGE criterion string for `bySpec`, for a
|
|
70
|
+
* NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
|
|
71
|
+
* `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
|
|
72
|
+
*
|
|
73
|
+
* BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
|
|
74
|
+
* because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
|
|
75
|
+
* and because inclusive-both-ends means adjacent ranges overlap at the join
|
|
76
|
+
* rather than leaving a gap that hides products.
|
|
77
|
+
*
|
|
78
|
+
* At least one bound is REQUIRED: a range with neither is a presence check, and
|
|
79
|
+
* this throws rather than sending a criterion the server would have to refuse.
|
|
80
|
+
* Discover which attributes take a range — and the span they actually occupy —
|
|
81
|
+
* from `specificationFacets()`: those are the facets with `kind: "numeric"`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // light paints, every brand at once
|
|
85
|
+
* client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
|
|
86
|
+
* @example
|
|
87
|
+
* // a mid-tone band
|
|
88
|
+
* specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
|
|
89
|
+
*/
|
|
90
|
+
export function specRangeCriterion(attributeId, bounds) {
|
|
91
|
+
const { min, max } = bounds;
|
|
92
|
+
if (min === undefined && max === undefined) {
|
|
93
|
+
throw new Error(`specRangeCriterion("${attributeId}") needs a min, a max, or both — a range with neither would match every recorded value. Use hasSpec() for a presence check.`);
|
|
94
|
+
}
|
|
95
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
96
|
+
throw new Error(`specRangeCriterion("${attributeId}") is inverted (min ${min} > max ${max}) and can only ever match nothing.`);
|
|
97
|
+
}
|
|
98
|
+
return `${attributeId}:${min ?? ""}..${max ?? ""}`;
|
|
99
|
+
}
|
|
68
100
|
/**
|
|
69
101
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
70
102
|
* certification PROGRAMME across all four certification buckets.
|
package/sse.js
CHANGED
|
@@ -40,7 +40,9 @@ export class SseDecoder {
|
|
|
40
40
|
flush() {
|
|
41
41
|
if (this.#buffer.length > 0) {
|
|
42
42
|
// At end of stream a deferred trailing CR is a real terminator, not data.
|
|
43
|
-
const line = this.#buffer.endsWith("\r")
|
|
43
|
+
const line = this.#buffer.endsWith("\r")
|
|
44
|
+
? this.#buffer.slice(0, -1)
|
|
45
|
+
: this.#buffer;
|
|
44
46
|
this.#buffer = "";
|
|
45
47
|
const frame = this.#consumeLine(line);
|
|
46
48
|
if (frame)
|
package/suggest-types.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* Undefined members are dropped by the transport.
|
|
22
22
|
*/
|
|
23
23
|
export function suggestQuery(q, options) {
|
|
24
|
-
const kind = Array.isArray(options?.kind)
|
|
24
|
+
const kind = Array.isArray(options?.kind)
|
|
25
|
+
? options.kind.join(",")
|
|
26
|
+
: options?.kind;
|
|
25
27
|
return { q, limit: options?.limit, kind };
|
|
26
28
|
}
|
package/types.d.ts
CHANGED
|
@@ -121,10 +121,14 @@ export interface SearchResultItem {
|
|
|
121
121
|
brandSlug: string | null;
|
|
122
122
|
productId: ProductId | null;
|
|
123
123
|
/**
|
|
124
|
-
* The variant's Material Bank SKU — the
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
124
|
+
* The variant's Material Bank SKU — the ONE EXTERNAL identifier on a result,
|
|
125
|
+
* in deliberate contrast to the opaque `variantId` above. `variantId` is ours
|
|
126
|
+
* and means nothing anywhere else; this is the id you are expected to match
|
|
127
|
+
* against Material Bank's own catalogue, so a result list can be joined to a
|
|
128
|
+
* cart, an order or an analytics event without a per-result lookup.
|
|
129
|
+
*
|
|
130
|
+
* Populated on EVERY lane: `search()` (all four arms), `match()`, and
|
|
131
|
+
* `matchStream()` frames.
|
|
128
132
|
*
|
|
129
133
|
* ABSENT means no MB identity is known for the variant — never that the SKU
|
|
130
134
|
* cannot be ordered. Presence is the orderability tell; absence is silence.
|
|
@@ -189,6 +193,17 @@ export interface SearchResultItem {
|
|
|
189
193
|
* dropped by default. Appended after the kept results.
|
|
190
194
|
*/
|
|
191
195
|
belowFloor?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Present (true) only on Match rows the colour probe admitted (match only):
|
|
198
|
+
* this material is as close in colour as the catalogue gets, and its image
|
|
199
|
+
* similarity is BELOW the match quality floor by design. It is on the page
|
|
200
|
+
* because nothing image-retrieved is nearer in colour, not because it cleared
|
|
201
|
+
* the floor — present it as a colour answer or suppress it knowingly. Absent
|
|
202
|
+
* (never `false`) on rows that came through image retrieval. Distinct from
|
|
203
|
+
* {@link SearchResultItem.belowFloor}, which marks rejects returned only under
|
|
204
|
+
* `includeBelowFloor`.
|
|
205
|
+
*/
|
|
206
|
+
admittedOnColour?: boolean;
|
|
192
207
|
siblingCount?: number;
|
|
193
208
|
visualMeanings?: VisualMeaning[];
|
|
194
209
|
/**
|
|
@@ -567,12 +582,24 @@ export interface SearchInput {
|
|
|
567
582
|
mood?: string;
|
|
568
583
|
sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
|
|
569
584
|
/**
|
|
570
|
-
* Seed a visual search from an existing variant's image (`variantId`)
|
|
571
|
-
*
|
|
572
|
-
*
|
|
585
|
+
* Seed a visual search from an existing variant's image (`variantId`), from a
|
|
586
|
+
* Material Bank SKU (`materialBankId`), or from a pre-computed image vector
|
|
587
|
+
* (`vector`).
|
|
588
|
+
*
|
|
589
|
+
* `materialBankId` exists so a caller already holding a SKU does not have to
|
|
590
|
+
* ask us to translate it before it can ask us anything useful — one call, not
|
|
591
|
+
* two. It resolves to the variant server-side and then behaves identically to
|
|
592
|
+
* the `{ variantId }` form, seed verdicts included. A SKU that names no
|
|
593
|
+
* variant is a 400 carrying the offending value: never an empty result set,
|
|
594
|
+
* never a silent fall back to unseeded search.
|
|
595
|
+
*
|
|
596
|
+
* The `{ vector }` form carries no seed variant, so it is the only one
|
|
597
|
+
* `verdicts` cannot diff a candidate against.
|
|
573
598
|
*/
|
|
574
599
|
similarTo?: {
|
|
575
600
|
variantId: VariantId;
|
|
601
|
+
} | {
|
|
602
|
+
materialBankId: string;
|
|
576
603
|
} | {
|
|
577
604
|
vector: string;
|
|
578
605
|
};
|
|
@@ -691,6 +718,13 @@ export interface ResolveAttributeOptions {
|
|
|
691
718
|
/** Options for `search/specifications`. */
|
|
692
719
|
export interface BySpecOptions {
|
|
693
720
|
limit?: number;
|
|
721
|
+
/**
|
|
722
|
+
* Numeric range criteria as `attribute:min..max` strings, ANDed with the
|
|
723
|
+
* `spec` criteria. Build them with `specRangeCriterion` rather than by hand —
|
|
724
|
+
* both bounds are inclusive and at least one is required, and a hand-built
|
|
725
|
+
* token with neither is refused by the server rather than matching everything.
|
|
726
|
+
*/
|
|
727
|
+
ranges?: string[];
|
|
694
728
|
}
|
|
695
729
|
export type * from "./demand-types.js";
|
|
696
730
|
/** One quarter's tally. Sparse across a series — an absent quarter is not a zero. */
|
|
@@ -731,31 +765,7 @@ export interface ExtractedPalette {
|
|
|
731
765
|
export interface ExtractPaletteResponse {
|
|
732
766
|
palette: ExtractedPalette | null;
|
|
733
767
|
}
|
|
734
|
-
export
|
|
735
|
-
value: string;
|
|
736
|
-
variants: number;
|
|
737
|
-
brands: number;
|
|
738
|
-
label?: string;
|
|
739
|
-
}
|
|
740
|
-
export interface SpecFacet {
|
|
741
|
-
attributeId: string;
|
|
742
|
-
name: string;
|
|
743
|
-
isArray: boolean;
|
|
744
|
-
brands: number;
|
|
745
|
-
values: SpecFacetValue[];
|
|
746
|
-
description?: string;
|
|
747
|
-
}
|
|
748
|
-
export interface SpecFacetsListResponse {
|
|
749
|
-
mode: "facets";
|
|
750
|
-
count: number;
|
|
751
|
-
facets: SpecFacet[];
|
|
752
|
-
}
|
|
753
|
-
export interface SpecFacetsDistributionResponse {
|
|
754
|
-
mode: "distribution";
|
|
755
|
-
attributeId: string;
|
|
756
|
-
count: number;
|
|
757
|
-
values: SpecFacetValue[];
|
|
758
|
-
}
|
|
768
|
+
export type * from "./spec-facet-types.js";
|
|
759
769
|
export interface ApplicableSpecAttribute {
|
|
760
770
|
id: string;
|
|
761
771
|
name: string;
|