@instruments/materialgraph 0.2.0 → 0.4.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/by-spec-types.d.ts +73 -0
- package/by-spec-types.js +7 -0
- package/client-browse.d.ts +46 -0
- package/client-browse.js +42 -0
- package/client.d.ts +83 -28
- package/client.js +99 -47
- package/codes-client.d.ts +82 -0
- package/codes-client.js +73 -0
- package/codes-types.d.ts +154 -9
- package/colour-aggregate-types.d.ts +93 -0
- package/colour-aggregate-types.js +1 -0
- package/colours-client.d.ts +52 -0
- package/colours-client.js +39 -0
- package/composition-types.d.ts +228 -2
- package/http-transport.d.ts +19 -2
- package/http-transport.js +72 -2
- package/index.d.ts +7 -0
- package/index.js +3 -0
- package/match-stream-types.d.ts +150 -0
- package/match-stream-types.js +28 -0
- package/match-stream.d.ts +22 -0
- package/match-stream.js +280 -0
- package/material-asset-types.d.ts +1 -1
- package/material-block-types.d.ts +23 -2
- package/material-types.d.ts +92 -0
- package/material-types.js +1 -0
- package/ndjson.d.ts +46 -0
- package/ndjson.js +103 -0
- package/operations.d.ts +1 -1
- package/operations.js +7 -0
- package/package.json +1 -1
- package/run-types.d.ts +65 -1
- package/runs.d.ts +45 -3
- package/runs.js +82 -43
- package/schedule-types.d.ts +88 -0
- package/schedule-types.js +12 -0
- package/search-refinement-types.d.ts +13 -0
- package/search-result-validation.d.ts +27 -0
- package/search-result-validation.js +174 -0
- package/source-material-validation.d.ts +3 -0
- package/source-material-validation.js +109 -0
- package/spec-types.d.ts +4 -0
- package/spec-types.js +4 -0
- package/specifications.d.ts +9 -0
- package/specifications.js +9 -0
- package/suggest-types.d.ts +58 -0
- package/suggest-types.js +21 -0
- package/types.d.ts +60 -73
- package/validation.d.ts +119 -0
- package/validation.js +241 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime validator for `GET /api/v1/source-material/{batchId}` — the
|
|
3
|
+
* hand-rolled, zero-runtime-dependency mirror of
|
|
4
|
+
* `sourceMaterialBatchDetailSchema` and its transitive closure
|
|
5
|
+
* (`sourceMaterialBatchDetailFileSchema`, `sourceImageAnalysisSchema`,
|
|
6
|
+
* `sourceImageAnalysisResultSchema`) in
|
|
7
|
+
* `packages/sources/src/source-material/contract.ts`.
|
|
8
|
+
*
|
|
9
|
+
* WHY IT EXISTS. `contract-parity.test.ts` proves the SDK's STATIC types match
|
|
10
|
+
* the canonical schemas; that is a compile-time proof about shapes and validates
|
|
11
|
+
* nothing at run time. This is the run-time half, and it must ACCEPT and REJECT
|
|
12
|
+
* exactly what the schema does — pinned by `source-material-parity.test.ts`.
|
|
13
|
+
*
|
|
14
|
+
* TWO CONTRACT PROPERTIES IT ENFORCES, both easy to lose in a refactor:
|
|
15
|
+
* 1. `analyses` is REQUIRED and an array on every file. `[]` means "nothing
|
|
16
|
+
* computed yet"; an absent field is a broken response, not an empty one.
|
|
17
|
+
* Preprocessing is manual today and automatic later — only the trigger
|
|
18
|
+
* changes, never this contract.
|
|
19
|
+
* 2. `result` is a JSON record all the way down (`z.json()`), not `unknown`.
|
|
20
|
+
* See `requireJsonValue` for why the schema is that strict and why this
|
|
21
|
+
* mirror cannot actually fire over the wire.
|
|
22
|
+
*
|
|
23
|
+
* ENUMS ARE CLOSED here, matching the canonical `z.enum(…)` members, while the
|
|
24
|
+
* published types are `OpenEnum` so a newer server value keeps type-checking.
|
|
25
|
+
* The validator follows the SCHEMA: the type stays wider than what is proven,
|
|
26
|
+
* which is imprecise but never a lie — the same choice
|
|
27
|
+
* `search-result-validation.ts` makes for `basis`.
|
|
28
|
+
*
|
|
29
|
+
* Package-internal (not re-exported from `index.ts`): a published symbol is a
|
|
30
|
+
* permanent contract, and promoting it later is additive.
|
|
31
|
+
*/
|
|
32
|
+
import { MaterialGraphError } from "./errors.js";
|
|
33
|
+
import { requireArray, requireEnum, requireSafeInteger, requireJsonRecord, requireNonEmptyString, requireRecord, requireString, } from "./validation.js";
|
|
34
|
+
/** `sourceImageAnalysisKindSchema` members, verbatim. */
|
|
35
|
+
const ANALYSIS_KINDS = ["scene_match"];
|
|
36
|
+
/** `sourceImageAnalysisStatusSchema` members, verbatim. */
|
|
37
|
+
const ANALYSIS_STATUSES = ["ok", "failed"];
|
|
38
|
+
/** `sourceMaterialRoleSchema` members, verbatim. */
|
|
39
|
+
const SOURCE_ROLES = [
|
|
40
|
+
"brief",
|
|
41
|
+
"reference_image",
|
|
42
|
+
"document",
|
|
43
|
+
"document_image",
|
|
44
|
+
"inspiration_image",
|
|
45
|
+
"unclassified",
|
|
46
|
+
];
|
|
47
|
+
function detailFailure(batchId) {
|
|
48
|
+
return (reason) => MaterialGraphError.unexpected(`GET /api/v1/source-material/${batchId} returned a malformed body: ${reason}`);
|
|
49
|
+
}
|
|
50
|
+
function checkAnalysis(value, what, fail) {
|
|
51
|
+
const record = requireRecord(value, what, fail);
|
|
52
|
+
// `model` and `analysedAt` ARE `.min(1)` canonically — fields this endpoint
|
|
53
|
+
// introduces, where an empty value is meaningless and `model` already cannot
|
|
54
|
+
// be written empty. Parity here is achieved by the schema being strict, never
|
|
55
|
+
// by this mirror freelancing past it.
|
|
56
|
+
return {
|
|
57
|
+
kind: requireEnum(record.kind, ANALYSIS_KINDS, `${what}.kind`, fail),
|
|
58
|
+
model: requireNonEmptyString(record.model, `${what}.model`, fail),
|
|
59
|
+
status: requireEnum(record.status, ANALYSIS_STATUSES, `${what}.status`, fail),
|
|
60
|
+
result: requireJsonRecord(record.result, `${what}.result`, fail),
|
|
61
|
+
analysedAt: requireNonEmptyString(record.analysedAt, `${what}.analysedAt`, fail),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function checkFile(value, what, fail) {
|
|
65
|
+
const record = requireRecord(value, what, fail);
|
|
66
|
+
// NOT `requireDefaultedArray`: the schema has no `.default([])`, so an absent
|
|
67
|
+
// `analyses` is a malformed body rather than an empty one. Keeping that
|
|
68
|
+
// distinction is the whole point of the always-an-array contract.
|
|
69
|
+
const analyses = requireArray(record.analyses, `${what}.analyses`, fail);
|
|
70
|
+
// `requireString`, NOT `requireNonEmptyString`, for the fields this endpoint
|
|
71
|
+
// INHERITS from the shared source-material contract (`sourceId`, `sha256`,
|
|
72
|
+
// `url` — plus `batchId`/`createdAt` below). Those are plain `z.string()`
|
|
73
|
+
// there and shared with the upload path; a client that rejected `""` would
|
|
74
|
+
// refuse a response the server can legitimately produce, and tightening them
|
|
75
|
+
// is a change to a contract this endpoint does not own.
|
|
76
|
+
return {
|
|
77
|
+
sourceId: requireString(record.sourceId, `${what}.sourceId`, fail),
|
|
78
|
+
role: requireEnum(record.role, SOURCE_ROLES, `${what}.role`, fail),
|
|
79
|
+
filename: requireString(record.filename, `${what}.filename`, fail),
|
|
80
|
+
mediaType: requireString(record.mediaType, `${what}.mediaType`, fail),
|
|
81
|
+
sha256: requireString(record.sha256, `${what}.sha256`, fail),
|
|
82
|
+
// `requireSafeInteger`, NOT `requireIntegerAtLeast(…, 0)`. Canonically these
|
|
83
|
+
// are a bare `z.number().int()` on the INHERITED
|
|
84
|
+
// `sourceMaterialFileReceiptSchema`, which carries no lower bound — so a
|
|
85
|
+
// negative is accepted there and must be accepted here.
|
|
86
|
+
//
|
|
87
|
+
// A negative `bytes` is nonsense and a negative `pages` more so, and this is
|
|
88
|
+
// still not the place to say so: a client validator holding an opinion the
|
|
89
|
+
// contract does not is how the two drift, and how a response the server may
|
|
90
|
+
// legitimately produce starts throwing at the boundary. Tightening those
|
|
91
|
+
// fields is a canonical change to a schema shared with the upload path, and
|
|
92
|
+
// belongs in its own ticket with this mirror following after.
|
|
93
|
+
bytes: requireSafeInteger(record.bytes, `${what}.bytes`, fail),
|
|
94
|
+
pages: record.pages === null ? null : requireSafeInteger(record.pages, `${what}.pages`, fail),
|
|
95
|
+
url: requireString(record.url, `${what}.url`, fail),
|
|
96
|
+
analyses: analyses.map((analysis, index) => checkAnalysis(analysis, `${what}.analyses[${index}]`, fail)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Validate a batch-detail body, returning it typed, or throw explaining why. */
|
|
100
|
+
export function checkSourceMaterialBatchDetail(value, batchId) {
|
|
101
|
+
const fail = detailFailure(batchId);
|
|
102
|
+
const record = requireRecord(value, "body", fail);
|
|
103
|
+
const sources = requireArray(record.sources, "body.sources", fail);
|
|
104
|
+
return {
|
|
105
|
+
batchId: requireString(record.batchId, "body.batchId", fail),
|
|
106
|
+
createdAt: requireString(record.createdAt, "body.createdAt", fail),
|
|
107
|
+
sources: sources.map((file, index) => checkFile(file, `body.sources[${index}]`, fail)),
|
|
108
|
+
};
|
|
109
|
+
}
|
package/spec-types.d.ts
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* in the last). A caller shouldn't have to know which bucket a programme fell
|
|
31
31
|
* into, so this single virtual id matches the `programme` term across all four.
|
|
32
32
|
*
|
|
33
|
+
* The `term` may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
|
|
34
|
+
* server-side to its canonical programme token, or a canonical token directly
|
|
35
|
+
* (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
|
|
36
|
+
*
|
|
33
37
|
* @example
|
|
34
38
|
* // "find a GREENGUARD-certified alternative"
|
|
35
39
|
* client.search({
|
package/spec-types.js
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* in the last). A caller shouldn't have to know which bucket a programme fell
|
|
31
31
|
* into, so this single virtual id matches the `programme` term across all four.
|
|
32
32
|
*
|
|
33
|
+
* The `term` may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
|
|
34
|
+
* server-side to its canonical programme token, or a canonical token directly
|
|
35
|
+
* (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
|
|
36
|
+
*
|
|
33
37
|
* @example
|
|
34
38
|
* // "find a GREENGUARD-certified alternative"
|
|
35
39
|
* client.search({
|
package/specifications.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export declare function hasSpec(attributeId: string): SpecPresenceFilter;
|
|
|
38
38
|
* virtual attribute so a programme matches wherever it landed at ingest
|
|
39
39
|
* (compliance / sustainability / material-health / VOC buckets).
|
|
40
40
|
*
|
|
41
|
+
* The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
|
|
42
|
+
* server resolves it to its canonical programme token before matching — or a
|
|
43
|
+
* canonical token directly (e.g. `greenguard_certificate`,
|
|
44
|
+
* `has_greenguard_gold_certification`).
|
|
45
|
+
*
|
|
41
46
|
* @example
|
|
42
47
|
* // "find a GREENGUARD-certified alternative"
|
|
43
48
|
* client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
|
|
@@ -56,6 +61,10 @@ export declare function specCriterion(attributeId: string, value: string): strin
|
|
|
56
61
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
57
62
|
* certification PROGRAMME across all four certification buckets.
|
|
58
63
|
*
|
|
64
|
+
* The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
|
|
65
|
+
* server-side to its canonical programme token, or a canonical token directly
|
|
66
|
+
* (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
|
|
67
|
+
*
|
|
59
68
|
* @example
|
|
60
69
|
* // every brand's GREENGUARD-Gold materials
|
|
61
70
|
* client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
|
package/specifications.js
CHANGED
|
@@ -42,6 +42,11 @@ export function hasSpec(attributeId) {
|
|
|
42
42
|
* virtual attribute so a programme matches wherever it landed at ingest
|
|
43
43
|
* (compliance / sustainability / material-health / VOC buckets).
|
|
44
44
|
*
|
|
45
|
+
* The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
|
|
46
|
+
* server resolves it to its canonical programme token before matching — or a
|
|
47
|
+
* canonical token directly (e.g. `greenguard_certificate`,
|
|
48
|
+
* `has_greenguard_gold_certification`).
|
|
49
|
+
*
|
|
45
50
|
* @example
|
|
46
51
|
* // "find a GREENGUARD-certified alternative"
|
|
47
52
|
* client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
|
|
@@ -64,6 +69,10 @@ export function specCriterion(attributeId, value) {
|
|
|
64
69
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
65
70
|
* certification PROGRAMME across all four certification buckets.
|
|
66
71
|
*
|
|
72
|
+
* The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
|
|
73
|
+
* server-side to its canonical programme token, or a canonical token directly
|
|
74
|
+
* (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
|
|
75
|
+
*
|
|
67
76
|
* @example
|
|
68
77
|
* // every brand's GREENGUARD-Gold materials
|
|
69
78
|
* client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity typeahead (MG-654) — the hand-mirrored wire types for
|
|
3
|
+
* `GET /api/v1/search/suggest`, plus the one query-shaping rule the endpoint
|
|
4
|
+
* imposes.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `types.ts` for the repository's source-file size budget, the same
|
|
7
|
+
* reason `catalog-discovery-types.ts` sits beside it; both are re-exported from
|
|
8
|
+
* `types.ts`, so importers see no difference.
|
|
9
|
+
*/
|
|
10
|
+
/** Which kind of entity a suggestion is — and therefore what `refId` means. */
|
|
11
|
+
export type SuggestKind = "product" | "brand" | "category";
|
|
12
|
+
/** How the query met the suggestion's label. Strongest first. */
|
|
13
|
+
export type SuggestMatch = "prefix" | "word" | "contains" | "fuzzy";
|
|
14
|
+
/** One ranked typeahead suggestion, carrying the identity it resolves to. */
|
|
15
|
+
export interface Suggestion {
|
|
16
|
+
kind: SuggestKind;
|
|
17
|
+
/** The entity's name as written, for display. */
|
|
18
|
+
label: string;
|
|
19
|
+
/**
|
|
20
|
+
* The resolvable identity: a product id, a brand slug, or a product-type key.
|
|
21
|
+
* Selecting a suggestion should navigate to this, not re-search `label`.
|
|
22
|
+
*/
|
|
23
|
+
refId: string;
|
|
24
|
+
match: SuggestMatch;
|
|
25
|
+
brandSlug: string | null;
|
|
26
|
+
brandName: string | null;
|
|
27
|
+
categoryKey: string | null;
|
|
28
|
+
/** A representative variant, so a client can open a material record directly. */
|
|
29
|
+
variantId: string | null;
|
|
30
|
+
variantCount: number;
|
|
31
|
+
}
|
|
32
|
+
/** The suggest response: the query as interpreted, plus the ranked suggestions. */
|
|
33
|
+
export interface SuggestResponse {
|
|
34
|
+
/** The normalised query the corpus was matched against. */
|
|
35
|
+
query: string;
|
|
36
|
+
/** Ranked best-match-first. Empty when nothing came close — a miss, not an error. */
|
|
37
|
+
suggestions: Suggestion[];
|
|
38
|
+
}
|
|
39
|
+
/** Options for {@link MaterialGraphClient.suggest}. */
|
|
40
|
+
export interface SuggestOptions {
|
|
41
|
+
/** How many suggestions to return (1–20, default 8). */
|
|
42
|
+
limit?: number;
|
|
43
|
+
/** Restrict to these kinds. Omit for all of them. */
|
|
44
|
+
kind?: SuggestKind | SuggestKind[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Shape the query string for a suggest call.
|
|
48
|
+
*
|
|
49
|
+
* The only non-obvious rule: `kind` is a COMMA LIST on the wire (`product,brand`),
|
|
50
|
+
* which is the house convention for multi-value v1 params, so an array is joined
|
|
51
|
+
* here rather than being sent as repeated `?kind=` pairs the route would ignore.
|
|
52
|
+
* Undefined members are dropped by the transport.
|
|
53
|
+
*/
|
|
54
|
+
export declare function suggestQuery(q: string, options?: SuggestOptions): {
|
|
55
|
+
q: string;
|
|
56
|
+
limit?: number;
|
|
57
|
+
kind?: string;
|
|
58
|
+
};
|
package/suggest-types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity typeahead (MG-654) — the hand-mirrored wire types for
|
|
3
|
+
* `GET /api/v1/search/suggest`, plus the one query-shaping rule the endpoint
|
|
4
|
+
* imposes.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `types.ts` for the repository's source-file size budget, the same
|
|
7
|
+
* reason `catalog-discovery-types.ts` sits beside it; both are re-exported from
|
|
8
|
+
* `types.ts`, so importers see no difference.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Shape the query string for a suggest call.
|
|
12
|
+
*
|
|
13
|
+
* The only non-obvious rule: `kind` is a COMMA LIST on the wire (`product,brand`),
|
|
14
|
+
* which is the house convention for multi-value v1 params, so an array is joined
|
|
15
|
+
* here rather than being sent as repeated `?kind=` pairs the route would ignore.
|
|
16
|
+
* Undefined members are dropped by the transport.
|
|
17
|
+
*/
|
|
18
|
+
export function suggestQuery(q, options) {
|
|
19
|
+
const kind = Array.isArray(options?.kind) ? options.kind.join(",") : options?.kind;
|
|
20
|
+
return { q, limit: options?.limit, kind };
|
|
21
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -12,9 +12,19 @@
|
|
|
12
12
|
* server value still type-checks, and objects that may grow fields are read
|
|
13
13
|
* structurally (unknown keys pass through untouched — the client never strips).
|
|
14
14
|
*/
|
|
15
|
+
import type { BySpecVerdict } from "./by-spec-types.js";
|
|
16
|
+
import type { MaterialDetail, MaterialTile } from "./material-types.js";
|
|
15
17
|
import type { SearchFilter, SearchSuggestion } from "./search-refinement-types.js";
|
|
16
18
|
/** A widened string-literal union: known members autocomplete, unknown values still type-check. */
|
|
17
19
|
export type OpenEnum<T extends string> = T | (string & {});
|
|
20
|
+
/**
|
|
21
|
+
* A JSON value, recursively — exactly what survives a `jsonb` round-trip
|
|
22
|
+
* unchanged. Used where a payload's SHAPE belongs to its producer but its values
|
|
23
|
+
* must still be JSON: `Date`, `bigint`, `undefined`, and functions are not.
|
|
24
|
+
*/
|
|
25
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
26
|
+
[key: string]: JsonValue;
|
|
27
|
+
};
|
|
18
28
|
/** One palette swatch of a material's main image. */
|
|
19
29
|
export interface PaletteSwatch {
|
|
20
30
|
hex: string;
|
|
@@ -48,60 +58,15 @@ export interface MaterialColour {
|
|
|
48
58
|
}
|
|
49
59
|
/** Where the served image came from. */
|
|
50
60
|
export type ImageSource = OpenEnum<"r2" | "origin">;
|
|
51
|
-
|
|
52
|
-
export interface MaterialTile {
|
|
53
|
-
variantId: string;
|
|
54
|
-
brand: string;
|
|
55
|
-
product: string;
|
|
56
|
-
variantName: string | null;
|
|
57
|
-
imageUrl: string | null;
|
|
58
|
-
imageSource: ImageSource | null;
|
|
59
|
-
category: string | null;
|
|
60
|
-
colour: MaterialColour;
|
|
61
|
-
}
|
|
62
|
-
/** A canonical attribute/term pair carried on a detail record. */
|
|
63
|
-
export interface MaterialFacet {
|
|
64
|
-
attributeId: string;
|
|
65
|
-
term: string;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* One dimensional configuration of a material. Numeric fields are millimetres
|
|
69
|
-
* (mass in kilograms); string fields are the source's own descriptors. The
|
|
70
|
-
* index signature preserves the corpus's long tail of rarer keys.
|
|
71
|
-
*/
|
|
72
|
-
export interface DimensionEntry {
|
|
73
|
-
label?: string;
|
|
74
|
-
tile_size?: string;
|
|
75
|
-
measurement_basis?: string;
|
|
76
|
-
width_mm?: number;
|
|
77
|
-
length_mm?: number;
|
|
78
|
-
height_mm?: number;
|
|
79
|
-
depth_mm?: number;
|
|
80
|
-
thickness_mm?: number;
|
|
81
|
-
diameter_mm?: number;
|
|
82
|
-
visible_projection_mm?: number;
|
|
83
|
-
weight_kg?: number;
|
|
84
|
-
[key: string]: unknown;
|
|
85
|
-
}
|
|
86
|
-
/** The record projection of a material — the tile plus detail-only fields. */
|
|
87
|
-
export interface MaterialDetail extends MaterialTile {
|
|
88
|
-
brandSlug: string;
|
|
89
|
-
productId: string;
|
|
90
|
-
color: string | null;
|
|
91
|
-
materialBankId: string | null;
|
|
92
|
-
installationImageUrl: string | null;
|
|
93
|
-
displayCategory: string | null;
|
|
94
|
-
categoryPath: string[];
|
|
95
|
-
palette: PaletteSwatch[];
|
|
96
|
-
facets: MaterialFacet[];
|
|
97
|
-
dimensions: DimensionEntry[] | null;
|
|
98
|
-
}
|
|
61
|
+
export type { DimensionEntry, MaterialAvailability, MaterialDetail, MaterialFacet, MaterialSuccessor, MaterialTile, } from "./material-types.js";
|
|
99
62
|
/** Per-signal scores, present on a result when `explain` was requested. */
|
|
100
63
|
export interface SearchResultSignals {
|
|
101
64
|
image: number | null;
|
|
102
65
|
category: number | null;
|
|
103
66
|
colour: number | null;
|
|
104
67
|
lexical: number | null;
|
|
68
|
+
/** Text→image RERANK cosine — non-null only for a seeded search carrying `text`. */
|
|
69
|
+
text: number | null;
|
|
105
70
|
}
|
|
106
71
|
/** An accepted/measured visual meaning — descriptive evidence, never a ranking cause. */
|
|
107
72
|
export interface VisualMeaning {
|
|
@@ -131,8 +96,21 @@ export interface SearchResultItem {
|
|
|
131
96
|
score: number;
|
|
132
97
|
signals?: SearchResultSignals;
|
|
133
98
|
retired?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Present (true) only on quality-floor rejects returned under `includeBelowFloor`
|
|
101
|
+
* (match only) — the item failed the match image-quality floor and would be
|
|
102
|
+
* dropped by default. Appended after the kept results.
|
|
103
|
+
*/
|
|
104
|
+
belowFloor?: boolean;
|
|
134
105
|
siblingCount?: number;
|
|
135
106
|
visualMeanings?: VisualMeaning[];
|
|
107
|
+
/**
|
|
108
|
+
* The shared report card for this hit against the request's `filters` (required
|
|
109
|
+
* cells) and `preferences` (preferred cells). Present (full view only) whenever a
|
|
110
|
+
* spec filter or preference was given. Reuses the by-spec verdict shape — the
|
|
111
|
+
* shared CandidateVerdict mirror; see {@link MaterialVerdict}.
|
|
112
|
+
*/
|
|
113
|
+
verdict?: BySpecVerdict;
|
|
136
114
|
}
|
|
137
115
|
/** The lean `view:"card"` result item — identity + name + thumbnail + score. */
|
|
138
116
|
export interface SearchCardItem {
|
|
@@ -385,31 +363,8 @@ export interface ResolveAttributeResponse {
|
|
|
385
363
|
candidates: AttributeCandidate[];
|
|
386
364
|
}
|
|
387
365
|
export type * from "./catalog-discovery-types.js";
|
|
388
|
-
export
|
|
389
|
-
|
|
390
|
-
confidence: number | null;
|
|
391
|
-
sourceBasis: string | null;
|
|
392
|
-
}
|
|
393
|
-
export interface BySpecMatch {
|
|
394
|
-
variantId: string;
|
|
395
|
-
brand: string;
|
|
396
|
-
product: string;
|
|
397
|
-
variant: string;
|
|
398
|
-
color: string | null;
|
|
399
|
-
imageUrl: string | null;
|
|
400
|
-
hex: string | null;
|
|
401
|
-
matched: BySpecMatchedCell[];
|
|
402
|
-
minConfidence: number | null;
|
|
403
|
-
}
|
|
404
|
-
export interface BySpecResponse {
|
|
405
|
-
criteria: Array<{
|
|
406
|
-
attribute: string;
|
|
407
|
-
value: string;
|
|
408
|
-
}>;
|
|
409
|
-
totalMatches: number;
|
|
410
|
-
brandsSpanned: number;
|
|
411
|
-
matches: BySpecMatch[];
|
|
412
|
-
}
|
|
366
|
+
export type * from "./suggest-types.js";
|
|
367
|
+
export type * from "./by-spec-types.js";
|
|
413
368
|
export interface WhoamiResponse {
|
|
414
369
|
principal: string;
|
|
415
370
|
scopes: string[];
|
|
@@ -453,6 +408,11 @@ export interface SearchInput {
|
|
|
453
408
|
};
|
|
454
409
|
mood?: string;
|
|
455
410
|
sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
|
|
411
|
+
/**
|
|
412
|
+
* Seed a visual search from an existing variant's image (`variantId`) or a
|
|
413
|
+
* pre-computed image vector (`vector`). Only the `{ variantId }` form carries a
|
|
414
|
+
* seed variant, so it is the only form `verdicts` can diff a candidate against.
|
|
415
|
+
*/
|
|
456
416
|
similarTo?: {
|
|
457
417
|
variantId: string;
|
|
458
418
|
} | {
|
|
@@ -468,8 +428,16 @@ export interface SearchInput {
|
|
|
468
428
|
category?: number;
|
|
469
429
|
colour?: number;
|
|
470
430
|
lexical?: number;
|
|
431
|
+
/** Text rerank multiplier applied when a seeded search also carries `text` (default 0.4). */
|
|
432
|
+
text?: number;
|
|
471
433
|
};
|
|
472
434
|
filters?: SearchFilter[];
|
|
435
|
+
/**
|
|
436
|
+
* SOFT spec criteria (rank-never-exclude). Same shape as `filters`; a preference
|
|
437
|
+
* never removes a result — it adds a `preferred` cell to each hit's `verdict` and
|
|
438
|
+
* demotes a failed/missing preferred hit WITHIN its page only.
|
|
439
|
+
*/
|
|
440
|
+
preferences?: SearchFilter[];
|
|
473
441
|
limit?: number;
|
|
474
442
|
cursor?: string;
|
|
475
443
|
explain?: boolean;
|
|
@@ -478,6 +446,13 @@ export interface SearchInput {
|
|
|
478
446
|
count?: boolean;
|
|
479
447
|
/** Only return materials with at least one catalog image. Default false (full catalog). */
|
|
480
448
|
hasImages?: boolean;
|
|
449
|
+
/**
|
|
450
|
+
* Opt-in seed-relative verdict cells (MG-629). Only meaningful with
|
|
451
|
+
* `similarTo: { variantId }`: each hit's `verdict` gains `preferred` cells
|
|
452
|
+
* diffing its comparable attributes (numeric + a `colour` axis) against the
|
|
453
|
+
* seed's, with directional deviations. Inert on every other seed form.
|
|
454
|
+
*/
|
|
455
|
+
verdicts?: boolean;
|
|
481
456
|
/** Result projection — `"full"` (default) or the lean `"card"` grid form. */
|
|
482
457
|
view?: OpenEnum<"full" | "card">;
|
|
483
458
|
}
|
|
@@ -534,7 +509,19 @@ export interface MatchInput {
|
|
|
534
509
|
detectionLabel?: string;
|
|
535
510
|
materialFunction?: string;
|
|
536
511
|
detectionCategory?: string;
|
|
512
|
+
/**
|
|
513
|
+
* Opt-in: also return quality-floor rejects, flagged `belowFloor: true` and
|
|
514
|
+
* appended after the kept results. Absent/false = default behaviour;
|
|
515
|
+
* `droppedBelowFloor` is unchanged.
|
|
516
|
+
*/
|
|
517
|
+
includeBelowFloor?: boolean;
|
|
537
518
|
requestedType?: OpenEnum<"swatch" | "installation" | "flatplan">;
|
|
519
|
+
/** Hard spec gates forwarded verbatim to the inner search — term match or presence, each with an optional trust bar. */
|
|
520
|
+
filters?: SearchFilter[];
|
|
521
|
+
/** Soft criteria (rank-never-exclude). A miss demotes within the page but never removes a hit. */
|
|
522
|
+
preferences?: SearchFilter[];
|
|
523
|
+
/** Opt in to seed-relative verdict cells; inert unless the inner search carries a variant seed. */
|
|
524
|
+
verdicts?: boolean;
|
|
538
525
|
}
|
|
539
526
|
/** Options for a batch material read. */
|
|
540
527
|
export interface MaterialsBatchOptions {
|
package/validation.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's shared wire-validation primitives — the vocabulary every hand-rolled
|
|
3
|
+
* mirror of a canonical Zod schema is built from.
|
|
4
|
+
*
|
|
5
|
+
* The SDK publishes with ZERO runtime dependencies, so it cannot call
|
|
6
|
+
* `schema.parse()` on anything. Where a wire payload must be proven rather than
|
|
7
|
+
* trusted, it is checked with these helpers and the result is pinned against the
|
|
8
|
+
* real schema in `contract-parity.test.ts`. Each helper mirrors one Zod
|
|
9
|
+
* construct, named for it, so a reviewer can read the mirror against the schema
|
|
10
|
+
* line by line.
|
|
11
|
+
*
|
|
12
|
+
* Every helper takes the same three shapes: the value, a `what` path used in the
|
|
13
|
+
* diagnostic (`results[3].verdict.cells[0].state`), and a {@link ValidationFailure}
|
|
14
|
+
* owned by the calling module — so one payload's failures all read alike without
|
|
15
|
+
* this file knowing what a Match frame or a Run event is.
|
|
16
|
+
*/
|
|
17
|
+
import { MaterialGraphError } from "./errors.js";
|
|
18
|
+
import type { JsonValue } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Builds the one error a validation run raises. The calling module supplies it,
|
|
21
|
+
* so the message names that payload and quotes its source.
|
|
22
|
+
*/
|
|
23
|
+
export type ValidationFailure = (reason: string) => MaterialGraphError;
|
|
24
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
25
|
+
/** A `z.object({…})`. */
|
|
26
|
+
export declare function requireRecord(value: unknown, what: string, fail: ValidationFailure): Record<string, unknown>;
|
|
27
|
+
/** A `z.string()`. */
|
|
28
|
+
export declare function requireString(value: unknown, what: string, fail: ValidationFailure): string;
|
|
29
|
+
/** A `z.string().min(1)`. */
|
|
30
|
+
export declare function requireNonEmptyString(value: unknown, what: string, fail: ValidationFailure): string;
|
|
31
|
+
/** A `z.string().optional()`: absent is fine, present-but-wrong is not. */
|
|
32
|
+
export declare function requireOptionalString(value: unknown, what: string, fail: ValidationFailure): void;
|
|
33
|
+
/** A `z.string().nullable()`. */
|
|
34
|
+
export declare function requireNullableString(value: unknown, what: string, fail: ValidationFailure): string | null;
|
|
35
|
+
/** A `z.string().min(1).nullable()`. */
|
|
36
|
+
export declare function requireNullableNonEmptyString(value: unknown, what: string, fail: ValidationFailure): string | null;
|
|
37
|
+
/** A `z.boolean()`. */
|
|
38
|
+
export declare function requireBoolean(value: unknown, what: string, fail: ValidationFailure): boolean;
|
|
39
|
+
/** A `z.boolean().optional()`. */
|
|
40
|
+
export declare function requireOptionalBoolean(value: unknown, what: string, fail: ValidationFailure): void;
|
|
41
|
+
/**
|
|
42
|
+
* A `z.number()`. Zod 4 rejects `NaN` AND `±Infinity` for it, and `1e999` parses
|
|
43
|
+
* out of JSON as `Infinity`, so finiteness is a reachable check.
|
|
44
|
+
*/
|
|
45
|
+
export declare function requireNumber(value: unknown, what: string, fail: ValidationFailure): number;
|
|
46
|
+
/** A `z.number().optional()`. */
|
|
47
|
+
export declare function requireOptionalNumber(value: unknown, what: string, fail: ValidationFailure): void;
|
|
48
|
+
/** A `z.number().nullable()`. */
|
|
49
|
+
export declare function requireNullableNumber(value: unknown, what: string, fail: ValidationFailure): number | null;
|
|
50
|
+
/**
|
|
51
|
+
* A `z.number().min(min).max(max)`. `NaN` and both infinities fail the
|
|
52
|
+
* comparisons, exactly as the bounded Zod schema rejects them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function requireBoundedNumber(value: unknown, what: string, fail: ValidationFailure, min: number, max: number): number;
|
|
55
|
+
/** A `z.number().min(min).max(max).nullable()`. */
|
|
56
|
+
export declare function requireNullableBoundedNumber(value: unknown, what: string, fail: ValidationFailure, min: number, max: number): number | null;
|
|
57
|
+
/**
|
|
58
|
+
* A bare `z.number().int()` — integer, finite, and within the SAFE range, with
|
|
59
|
+
* NO lower bound.
|
|
60
|
+
*
|
|
61
|
+
* Measured against the pinned Zod (4.4.3), `.int()` is exactly
|
|
62
|
+
* `Number.isSafeInteger`: it accepts `0`, `-1`, and `±MAX_SAFE_INTEGER`, and
|
|
63
|
+
* rejects `1.5`, `±2**53`, `NaN`, and `±Infinity`.
|
|
64
|
+
*
|
|
65
|
+
* Two ways to get this wrong, both of which break parity in a different
|
|
66
|
+
* direction. Adding a `min: 0` REJECTS a negative the schema accepts — a client
|
|
67
|
+
* refusing a legitimate response. Using `Number.isInteger` ACCEPTS `2**53`,
|
|
68
|
+
* which the schema rejects — so `requireIntegerAtLeast` is not this helper with
|
|
69
|
+
* the bound dropped, and the safe-range test is why.
|
|
70
|
+
*/
|
|
71
|
+
export declare function requireSafeInteger(value: unknown, what: string, fail: ValidationFailure): number;
|
|
72
|
+
/** A `z.number().int().min(min)` — the integer constraint AND the lower bound. */
|
|
73
|
+
export declare function requireIntegerAtLeast(value: unknown, what: string, fail: ValidationFailure, min: number): number;
|
|
74
|
+
/** A `z.enum([…])` — CLOSED, exactly like the canonical member list. */
|
|
75
|
+
export declare function requireEnum(value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure): string;
|
|
76
|
+
/** A `z.enum([…]).nullable()`. */
|
|
77
|
+
export declare function requireNullableEnum(value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure): string | null;
|
|
78
|
+
/** A `z.array(…)`. */
|
|
79
|
+
export declare function requireArray(value: unknown, what: string, fail: ValidationFailure): unknown[];
|
|
80
|
+
/**
|
|
81
|
+
* A `z.array(…).default([])`: an OMITTED array is valid and materialises as
|
|
82
|
+
* `[]`; a present non-array is not.
|
|
83
|
+
*/
|
|
84
|
+
export declare function requireDefaultedArray(value: unknown, what: string, fail: ValidationFailure): unknown[];
|
|
85
|
+
/** A `z.array(z.string()).min(1)` — non-empty, and every element a string. */
|
|
86
|
+
export declare function requireNonEmptyStringArray(value: unknown, what: string, fail: ValidationFailure): string[];
|
|
87
|
+
/**
|
|
88
|
+
* A `z.json()` — a JSON value, recursively: `string | number | boolean | null`,
|
|
89
|
+
* an array of them, or an object of them, at ANY depth.
|
|
90
|
+
*
|
|
91
|
+
* WHY THIS EXISTS, given it can never fire over the wire. Every SDK response
|
|
92
|
+
* has already been through `JSON.parse`, so a `Date`, `bigint`, `undefined`, or
|
|
93
|
+
* function CANNOT be present by construction — this check is unreachable in
|
|
94
|
+
* practice. It is here for PARITY: the canonical `sourceImageAnalysisResultSchema`
|
|
95
|
+
* uses `z.json()` precisely because those four break differently on the way to
|
|
96
|
+
* `jsonb` (a `bigint` throws in `JSON.stringify`, an `undefined` is silently
|
|
97
|
+
* dropped, and a `Date` round-trips as an ISO *string* — a different type read
|
|
98
|
+
* back than written). The mirror has to reject what the schema rejects or the
|
|
99
|
+
* parity test is lying. Do not delete this as dead code.
|
|
100
|
+
*/
|
|
101
|
+
export declare function requireJsonValue(value: unknown, what: string, fail: ValidationFailure): JsonValue;
|
|
102
|
+
/**
|
|
103
|
+
* A `z.record(z.string(), z.json())` — a PLAIN JSON object of JSON values.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately NOT `requireRecord` + walk the entries. That combination has a
|
|
106
|
+
* top-level hole: `Object.entries(new Date())` and `Object.entries(new Map())`
|
|
107
|
+
* are both `[]`, so there is nothing to recurse over and the value is accepted
|
|
108
|
+
* as `{}` — while `z.record(z.string(), z.json())` rejects both outright. A
|
|
109
|
+
* "non-array object" guard would close those two and still miss a class instance
|
|
110
|
+
* with real own properties, which canonical also rejects. Only a prototype test
|
|
111
|
+
* matches, and {@link requireJsonValue} already applies it at every depth — so
|
|
112
|
+
* this delegates there and then narrows to an object, rather than reimplementing
|
|
113
|
+
* the test and acquiring the same hole one level up.
|
|
114
|
+
*
|
|
115
|
+
* `requireRecord` is left alone: its other callers mirror `z.object({…})`, which
|
|
116
|
+
* genuinely does accept any record, and tightening a shared helper under them
|
|
117
|
+
* would be a change none of them asked for.
|
|
118
|
+
*/
|
|
119
|
+
export declare function requireJsonRecord(value: unknown, what: string, fail: ValidationFailure): Record<string, JsonValue>;
|