@instruments/materialgraph 0.12.0 → 0.12.1
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/browse-types.d.ts +4 -7
- package/by-colour-types.d.ts +2 -5
- package/corpus-scope-types.d.ts +55 -0
- package/corpus-scope-types.js +25 -0
- package/errors.d.ts +29 -0
- package/errors.js +40 -0
- package/exploration-types.d.ts +3 -5
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/search-refinement-types.d.ts +26 -3
- package/search-request-types.d.ts +13 -5
- package/types.d.ts +29 -5
package/browse-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ByColourSwatch } from "./by-colour-types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Wire types for the browse lane — GET `/api/v1/search/browse`.
|
|
3
4
|
*
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
* Nothing about the client's surface changed: `index.ts` re-exports these
|
|
13
14
|
* exactly as before.
|
|
14
15
|
*/
|
|
15
|
-
import type {
|
|
16
|
+
import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
16
17
|
import type { Cursor, ProductId, VariantId } from "./identifiers.js";
|
|
17
18
|
import type { CountedTotal, OpenEnum } from "./types.js";
|
|
18
19
|
/** One item from the pure colour-scalar browse. */
|
|
@@ -88,11 +89,7 @@ export interface BrowseResponse {
|
|
|
88
89
|
* narrowed it. ABSENT means the whole graph: every material MaterialGraph
|
|
89
90
|
* knows, orderable or not.
|
|
90
91
|
*/
|
|
91
|
-
corpusScope?:
|
|
92
|
-
scope: OpenEnum<"all" | "materialBankOrderable">;
|
|
93
|
-
/** What the scope admits, in one sentence, for a human reader. */
|
|
94
|
-
description: string;
|
|
95
|
-
};
|
|
92
|
+
corpusScope?: CorpusScopeReceipt;
|
|
96
93
|
}
|
|
97
94
|
/** Params for the pure colour `search/browse` surface. */
|
|
98
95
|
export interface BrowseParams {
|
|
@@ -143,5 +140,5 @@ export interface BrowseParams {
|
|
|
143
140
|
* scoped page is still a full page and `cursor` still means another eligible
|
|
144
141
|
* row exists.
|
|
145
142
|
*/
|
|
146
|
-
corpusScope?:
|
|
143
|
+
corpusScope?: CorpusScope;
|
|
147
144
|
}
|
package/by-colour-types.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* budget. Same tolerant-parsing contract as the rest of the mirror — response
|
|
7
7
|
* enums are widened so a new server value still type-checks.
|
|
8
8
|
*/
|
|
9
|
+
import type { CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
9
10
|
import type { VariantId } from "./identifiers.js";
|
|
10
11
|
import type { OklabTriple, OpenEnum, PaletteSwatch } from "./types.js";
|
|
11
12
|
/** Coarse group + fine type of a colour match's primary product type. */
|
|
@@ -186,11 +187,7 @@ export interface ByColourResponse {
|
|
|
186
187
|
* counts describe the SCOPED corpus too, because the scope was applied inside
|
|
187
188
|
* retrieval rather than over the page it produced.
|
|
188
189
|
*/
|
|
189
|
-
corpusScope?:
|
|
190
|
-
scope: OpenEnum<"all" | "materialBankOrderable">;
|
|
191
|
-
/** What the scope admits, in one sentence, for a human reader. */
|
|
192
|
-
description: string;
|
|
193
|
-
};
|
|
190
|
+
corpusScope?: CorpusScopeReceipt;
|
|
194
191
|
}
|
|
195
192
|
/** Params for `search/colours`. */
|
|
196
193
|
export interface ByColourParams {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The corpus scope vocabulary, declared ONCE for the whole client.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this file exists ──
|
|
5
|
+
* Until MG-1168 the union was spelled inline at six places across the SDK, and
|
|
6
|
+
* five of them had gone stale: `designShopOrderable` shipped in MG-1144 and
|
|
7
|
+
* reached only the search request type, so a TypeScript consumer could send the
|
|
8
|
+
* scope to `/search` and not to `/browse` — against a server that had accepted
|
|
9
|
+
* it on both since the day it landed. The narrowed RECEIPT types were worse
|
|
10
|
+
* than the request ones, because a caller could not even name the scope it got
|
|
11
|
+
* back.
|
|
12
|
+
*
|
|
13
|
+
* That is the third hop of the reachability checklist failing quietly: server
|
|
14
|
+
* correct, published client unable to reach the surface. One alias means the
|
|
15
|
+
* next member is one edit here rather than six edits nobody is reminded to
|
|
16
|
+
* make. `corpus-scope-parity.test.ts` in `apps/web/config` fails if this file
|
|
17
|
+
* falls behind `CORPUS_SCOPES`.
|
|
18
|
+
*
|
|
19
|
+
* ── Why `OpenEnum` ──
|
|
20
|
+
* The server may add a scope before a consumer upgrades. `OpenEnum` keeps the
|
|
21
|
+
* known members as autocomplete while still accepting a string the server knows
|
|
22
|
+
* and this build does not, so a new scope is not a compile error in a client
|
|
23
|
+
* that simply has not been republished yet.
|
|
24
|
+
*/
|
|
25
|
+
import { type OpenEnum } from "./types.js";
|
|
26
|
+
/**
|
|
27
|
+
* Every corpus a caller may ask for.
|
|
28
|
+
*
|
|
29
|
+
* `"all"` (and absence) is the WHOLE GRAPH — every material MaterialGraph
|
|
30
|
+
* knows, orderable or not. Absence is never a narrower default.
|
|
31
|
+
*
|
|
32
|
+
* Three members gate on CHANNEL membership, which is what "can someone buy
|
|
33
|
+
* this" means: `"materialBankNaOrderable"` (Material Bank North America),
|
|
34
|
+
* `"materialBankEuOrderable"` (Material Bank Europe) and
|
|
35
|
+
* `"designShopOrderable"` (DesignShop, with the products DesignShop hides from
|
|
36
|
+
* browse excluded).
|
|
37
|
+
*
|
|
38
|
+
* `"materialBankOrderable"` is NOT one of them, despite the name. It selects
|
|
39
|
+
* materials carrying a Material Bank identity — PROVENANCE, not a storefront —
|
|
40
|
+
* and admits about 98% of the graph. Use it to exclude the crawl-sourced brands
|
|
41
|
+
* that were never in the Material Bank feed; if you mean orderable, use one of
|
|
42
|
+
* the three above.
|
|
43
|
+
*/
|
|
44
|
+
export type CorpusScope = OpenEnum<"all" | "designShopOrderable" | "materialBankEuOrderable" | "materialBankNaOrderable" | "materialBankOrderable">;
|
|
45
|
+
/**
|
|
46
|
+
* The corpus a response AND its counts were drawn from. Present only when a
|
|
47
|
+
* narrowing scope applied — absence means the whole graph. Read it alongside
|
|
48
|
+
* the counts: a short page under a scope is a small SCOPED corpus, never a
|
|
49
|
+
* truncated one.
|
|
50
|
+
*/
|
|
51
|
+
export interface CorpusScopeReceipt {
|
|
52
|
+
scope: CorpusScope;
|
|
53
|
+
/** What the scope admits, in one sentence, for a human reader. */
|
|
54
|
+
description: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The corpus scope vocabulary, declared ONCE for the whole client.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this file exists ──
|
|
5
|
+
* Until MG-1168 the union was spelled inline at six places across the SDK, and
|
|
6
|
+
* five of them had gone stale: `designShopOrderable` shipped in MG-1144 and
|
|
7
|
+
* reached only the search request type, so a TypeScript consumer could send the
|
|
8
|
+
* scope to `/search` and not to `/browse` — against a server that had accepted
|
|
9
|
+
* it on both since the day it landed. The narrowed RECEIPT types were worse
|
|
10
|
+
* than the request ones, because a caller could not even name the scope it got
|
|
11
|
+
* back.
|
|
12
|
+
*
|
|
13
|
+
* That is the third hop of the reachability checklist failing quietly: server
|
|
14
|
+
* correct, published client unable to reach the surface. One alias means the
|
|
15
|
+
* next member is one edit here rather than six edits nobody is reminded to
|
|
16
|
+
* make. `corpus-scope-parity.test.ts` in `apps/web/config` fails if this file
|
|
17
|
+
* falls behind `CORPUS_SCOPES`.
|
|
18
|
+
*
|
|
19
|
+
* ── Why `OpenEnum` ──
|
|
20
|
+
* The server may add a scope before a consumer upgrades. `OpenEnum` keeps the
|
|
21
|
+
* known members as autocomplete while still accepting a string the server knows
|
|
22
|
+
* and this build does not, so a new scope is not a compile error in a client
|
|
23
|
+
* that simply has not been republished yet.
|
|
24
|
+
*/
|
|
25
|
+
import {} from "./types.js";
|
package/errors.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface ProblemDetail {
|
|
|
17
17
|
suggestions?: string[];
|
|
18
18
|
doc_uri?: string;
|
|
19
19
|
issues?: SanitizedIssue[];
|
|
20
|
+
/**
|
|
21
|
+
* MaterialGraph's documented RFC 7807 extension members, sanitized by the
|
|
22
|
+
* same rules as every other nested value here. Absent when the response did
|
|
23
|
+
* not carry them. See {@link MATERIALGRAPH_PROBLEM_EXTENSIONS}.
|
|
24
|
+
*/
|
|
25
|
+
readonly [extension: string]: unknown;
|
|
20
26
|
}
|
|
21
27
|
/** Normalized residual `{ error }` body. Unknown response fields are discarded. */
|
|
22
28
|
export interface SimpleErrorBody {
|
|
@@ -61,6 +67,29 @@ interface TransportErrorInput extends MaterialGraphRequestMetadata {
|
|
|
61
67
|
cause: unknown;
|
|
62
68
|
callerSignal?: AbortSignal;
|
|
63
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* MaterialGraph's documented problem EXTENSION members — the only non-standard
|
|
72
|
+
* keys this client retains.
|
|
73
|
+
*
|
|
74
|
+
* ── Why an allow-list and not "keep everything" ──
|
|
75
|
+
* Dropping unknown members is a deliberate decision here, pinned by a test that
|
|
76
|
+
* sends `unknown_top_level: "must-not-be-retained"` and asserts it is gone. A
|
|
77
|
+
* client should not echo arbitrary server-supplied keys into an object the
|
|
78
|
+
* caller trusts. That decision stands.
|
|
79
|
+
*
|
|
80
|
+
* ── Why not "drop everything", which is what it used to do ──
|
|
81
|
+
* MaterialGraph sends these members precisely so a caller can BRANCH on a
|
|
82
|
+
* refusal rather than parse its prose, and dropping them silently forced the
|
|
83
|
+
* opposite. A consumer reported it on 2026-08-21: they received
|
|
84
|
+
* `populated_alternatives` on the wire, found it absent from the thrown error,
|
|
85
|
+
* and were left matching a regex against `detail` — the exact coupling the
|
|
86
|
+
* field was added to remove. All ten members below were unreachable.
|
|
87
|
+
*
|
|
88
|
+
* So: the server's own contract is retained, and nothing else is. Adding a
|
|
89
|
+
* member to a problem response means adding it here too, and
|
|
90
|
+
* `problem-extension-parity.test.ts` fails when the two disagree.
|
|
91
|
+
*/
|
|
92
|
+
export declare const MATERIALGRAPH_PROBLEM_EXTENSIONS: readonly ["content_length", "did_you_mean", "empty_brands", "limit_bytes", "missing_env", "populated_alternatives", "unknown_attributes", "unknown_brands", "valid_group_keys", "valid_type_keys"];
|
|
64
93
|
/** The one error class thrown by all SDK transport and HTTP failures. */
|
|
65
94
|
export declare class MaterialGraphError extends Error {
|
|
66
95
|
readonly kind: MaterialGraphErrorKind;
|
package/errors.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
/** Error and rate-limit contracts for the retry-free MaterialGraph SDK. */
|
|
2
2
|
const RETRIABLE_STATUS = new Set([502, 503, 504]);
|
|
3
|
+
/**
|
|
4
|
+
* MaterialGraph's documented problem EXTENSION members — the only non-standard
|
|
5
|
+
* keys this client retains.
|
|
6
|
+
*
|
|
7
|
+
* ── Why an allow-list and not "keep everything" ──
|
|
8
|
+
* Dropping unknown members is a deliberate decision here, pinned by a test that
|
|
9
|
+
* sends `unknown_top_level: "must-not-be-retained"` and asserts it is gone. A
|
|
10
|
+
* client should not echo arbitrary server-supplied keys into an object the
|
|
11
|
+
* caller trusts. That decision stands.
|
|
12
|
+
*
|
|
13
|
+
* ── Why not "drop everything", which is what it used to do ──
|
|
14
|
+
* MaterialGraph sends these members precisely so a caller can BRANCH on a
|
|
15
|
+
* refusal rather than parse its prose, and dropping them silently forced the
|
|
16
|
+
* opposite. A consumer reported it on 2026-08-21: they received
|
|
17
|
+
* `populated_alternatives` on the wire, found it absent from the thrown error,
|
|
18
|
+
* and were left matching a regex against `detail` — the exact coupling the
|
|
19
|
+
* field was added to remove. All ten members below were unreachable.
|
|
20
|
+
*
|
|
21
|
+
* So: the server's own contract is retained, and nothing else is. Adding a
|
|
22
|
+
* member to a problem response means adding it here too, and
|
|
23
|
+
* `problem-extension-parity.test.ts` fails when the two disagree.
|
|
24
|
+
*/
|
|
25
|
+
export const MATERIALGRAPH_PROBLEM_EXTENSIONS = [
|
|
26
|
+
"content_length",
|
|
27
|
+
"did_you_mean",
|
|
28
|
+
"empty_brands",
|
|
29
|
+
"limit_bytes",
|
|
30
|
+
"missing_env",
|
|
31
|
+
"populated_alternatives",
|
|
32
|
+
"unknown_attributes",
|
|
33
|
+
"unknown_brands",
|
|
34
|
+
"valid_group_keys",
|
|
35
|
+
"valid_type_keys",
|
|
36
|
+
];
|
|
37
|
+
const PROBLEM_EXTENSION_KEYS = new Set(MATERIALGRAPH_PROBLEM_EXTENSIONS);
|
|
3
38
|
const MAX_UNEXPECTED_TEXT = 768;
|
|
4
39
|
const MAX_EXTENSION_DEPTH = 8;
|
|
5
40
|
const MAX_EXTENSION_ITEMS = 100;
|
|
@@ -89,7 +124,12 @@ function normalizeProblem(body, secrets) {
|
|
|
89
124
|
}
|
|
90
125
|
const suggestions = sanitizeSuggestions(body.suggestions, secrets);
|
|
91
126
|
const issues = sanitizeIssues(body.issues, secrets);
|
|
127
|
+
// Extensions FIRST so a malformed one can never shadow a standard member
|
|
128
|
+
// below. `sanitizeRecord` is the existing redaction and depth/item caps —
|
|
129
|
+
// this is that machinery finally being reached, not a second sanitizer.
|
|
130
|
+
const extensions = sanitizeRecord(Object.fromEntries(Object.entries(body).filter(([key]) => PROBLEM_EXTENSION_KEYS.has(key))), secrets, 0);
|
|
92
131
|
return {
|
|
132
|
+
...extensions,
|
|
93
133
|
type: safeString(body.type, secrets),
|
|
94
134
|
title: safeString(body.title, secrets),
|
|
95
135
|
status: body.status,
|
package/exploration-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
1
2
|
import type { VariantId } from "./identifiers.js";
|
|
2
3
|
export type GraphOperator = "colourways" | "same_category" | "still_listed" | "looks_like" | "same_colour" | "pairs_with" | "often_specified_with" | "substitutes_for" | "more_durable" | "outdoor_suitable" | "lower_cost" | "more_sustainable";
|
|
3
4
|
export interface GraphOperatorAvailability {
|
|
@@ -14,7 +15,7 @@ export interface GraphOperatorEvidence {
|
|
|
14
15
|
}
|
|
15
16
|
export interface CandidateReadInput {
|
|
16
17
|
operator: "looks_like";
|
|
17
|
-
corpusScope?:
|
|
18
|
+
corpusScope?: CorpusScope;
|
|
18
19
|
limit?: number;
|
|
19
20
|
excludeVariantIds?: VariantId[];
|
|
20
21
|
}
|
|
@@ -62,10 +63,7 @@ export interface CandidateSeedEvidence {
|
|
|
62
63
|
}
|
|
63
64
|
export interface CandidateReadReceipt {
|
|
64
65
|
operator: "looks_like";
|
|
65
|
-
corpusScope:
|
|
66
|
-
scope: "all" | "materialBankOrderable";
|
|
67
|
-
description: string;
|
|
68
|
-
} | null;
|
|
66
|
+
corpusScope: CorpusScopeReceipt | null;
|
|
69
67
|
requestedLimit: number;
|
|
70
68
|
returnedCount: number;
|
|
71
69
|
exclusions: {
|
package/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { PROJECT_EVENT_KINDS } from "./project-types.js";
|
|
|
14
14
|
export type * from "./project-types.js";
|
|
15
15
|
export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
|
|
16
16
|
export type * from "./run-events.js";
|
|
17
|
-
export { MaterialGraphError } from "./errors.js";
|
|
17
|
+
export { MATERIALGRAPH_PROBLEM_EXTENSIONS, MaterialGraphError, } from "./errors.js";
|
|
18
18
|
export type { MaterialGraphErrorInit, MaterialGraphErrorKind, MaterialGraphRequestMetadata, ProblemDetail, RateLimit, SanitizedErrorValue, SanitizedIssue, SimpleErrorBody, } from "./errors.js";
|
|
19
19
|
export type * from "./types.js";
|
|
20
20
|
export type * from "./match-types.js";
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { TERMINAL_RUN_EVENT_TYPES } from "./run-types.js";
|
|
|
4
4
|
export { VARY_AXES } from "./composition-types.js";
|
|
5
5
|
export { PROJECT_EVENT_KINDS } from "./project-types.js";
|
|
6
6
|
export { COMPOSITION_PHASE_EVENT_TYPES, COMPOSITION_PROGRAMME_EVENT_TYPES, COMPOSITION_REVIEW_CRITERIA_EVENT_TYPES, parseRunEvent, } from "./run-events.js";
|
|
7
|
-
export { MaterialGraphError } from "./errors.js";
|
|
7
|
+
export { MATERIALGRAPH_PROBLEM_EXTENSIONS, MaterialGraphError, } from "./errors.js";
|
|
8
8
|
export { MATCH_IMAGE_TYPES, MATCH_OUTCOMES, MATCH_MATERIAL_FUNCTIONS, MATCH_STREAM_EVENT_TYPES, MATCH_SURFACE_STATUSES, MATCH_WORKFLOW_STEPS, } from "./match-stream-types.js";
|
|
9
9
|
export { CodesClient } from "./codes-client.js";
|
|
10
10
|
export { ColoursClient } from "./colours-client.js";
|
package/package.json
CHANGED
|
@@ -28,14 +28,21 @@ export interface SearchFilterTrust {
|
|
|
28
28
|
minConfidence?: number;
|
|
29
29
|
/**
|
|
30
30
|
* Demand an EXACT source basis (closed enum — an unknown basis is rejected).
|
|
31
|
-
*
|
|
32
|
-
* - `tested` — a measured result. The strongest claim available.
|
|
31
|
+
* Strongest to weakest as evidence:
|
|
33
32
|
* - `declared` — the manufacturer stated it. Attributable, not independent.
|
|
34
33
|
* - `catalog_feed` — it arrived in a catalog feed. Trusted at ingest, unreviewed.
|
|
35
34
|
* - `derived` — MaterialGraph inferred it. Ours, not the manufacturer's — never
|
|
36
35
|
* present a `derived` value as something the brand claims.
|
|
36
|
+
*
|
|
37
|
+
* There is NO `tested` basis. This docblock advertised one as "the strongest
|
|
38
|
+
* claim available" through 0.12.0, and the union listed `declared` twice in
|
|
39
|
+
* its place — so a caller reading the documentation would write a value the
|
|
40
|
+
* server rejects with a 400, and the type would not stop them because the
|
|
41
|
+
* fourth arm was a duplicate rather than the value described.
|
|
42
|
+
* `packages/schema/src/source-basis.test.ts` asserts `tested` parses false;
|
|
43
|
+
* its absence is deliberate, not an omission.
|
|
37
44
|
*/
|
|
38
|
-
sourceBasis?: "catalog_feed" | "declared" | "derived"
|
|
45
|
+
sourceBasis?: "catalog_feed" | "declared" | "derived";
|
|
39
46
|
/** Fold the attribute's own registry confidence requirement in, server-side. */
|
|
40
47
|
meetsRequirement?: true;
|
|
41
48
|
}
|
|
@@ -49,6 +56,22 @@ export type SearchFilter = {
|
|
|
49
56
|
attributeId: string;
|
|
50
57
|
term: string;
|
|
51
58
|
trust?: SearchFilterTrust;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* TERM SET — the attribute must equal any ONE of these. OR within the entry;
|
|
62
|
+
* AND still holds between entries, so a filter rail composes the same way
|
|
63
|
+
* whatever it is filtering on.
|
|
64
|
+
*
|
|
65
|
+
* The server has accepted this since the term-set arm shipped, and a consumer
|
|
66
|
+
* has been sending it successfully on the wire — but it was missing from this
|
|
67
|
+
* union through 0.12.0, so multi-value filtering type-checked nowhere while
|
|
68
|
+
* working everywhere. Brand multi-select is the common case:
|
|
69
|
+
* `{attributeId: "brand", terms: ["kvadrat", "interface"]}`.
|
|
70
|
+
*/
|
|
71
|
+
| {
|
|
72
|
+
attributeId: string;
|
|
73
|
+
terms: string[];
|
|
74
|
+
trust?: SearchFilterTrust;
|
|
52
75
|
} | {
|
|
53
76
|
attributeId: string;
|
|
54
77
|
hasValue: true;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* repo's source-size gate; every name here is re-exported from `types.ts`, so
|
|
6
6
|
* no import path changes.
|
|
7
7
|
*/
|
|
8
|
+
import type { CorpusScope } from "./corpus-scope-types.js";
|
|
8
9
|
import type { Cursor, VariantId } from "./identifiers.js";
|
|
9
10
|
import type { SearchFilter } from "./search-refinement-types.js";
|
|
10
11
|
import type { ColourInput, OpenEnum, SearchImageInput } from "./types.js";
|
|
@@ -126,10 +127,17 @@ export interface SearchInput {
|
|
|
126
127
|
* **ABSENT means the WHOLE GRAPH** — every material MaterialGraph knows,
|
|
127
128
|
* orderable or not. Absence is never a narrower default, so a caller that
|
|
128
129
|
* says nothing gets materials it may not be able to buy.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
130
|
+
* Three scopes gate on CHANNEL membership, which is what "can someone buy
|
|
131
|
+
* this" means: `"materialBankNaOrderable"` (Material Bank North America,
|
|
132
|
+
* ~277,000 materials), `"materialBankEuOrderable"` (Material Bank Europe,
|
|
133
|
+
* ~65,000) and `"designShopOrderable"` (DesignShop, ~34,000, with the
|
|
134
|
+
* products DesignShop hides from browse excluded).
|
|
135
|
+
*
|
|
136
|
+
* `"materialBankOrderable"` is NOT one of them, despite the name. It selects
|
|
137
|
+
* materials carrying a Material Bank identity — a PROVENANCE signal, not a
|
|
138
|
+
* storefront — and admits about 98% of the graph. Use it to exclude the
|
|
139
|
+
* crawl-sourced brands that were never in the Material Bank feed; if you mean
|
|
140
|
+
* orderable, use one of the three above.
|
|
133
141
|
*
|
|
134
142
|
* A scope answers what is BUYABLE. It never answers what is SUITABLE: scope
|
|
135
143
|
* on channel, then refine on sector. A trade-only material can be right for a
|
|
@@ -142,7 +150,7 @@ export interface SearchInput {
|
|
|
142
150
|
* shortens the page exactly when the query drifts toward the corpus beyond
|
|
143
151
|
* the Material Bank feed, which is unpredictable from outside.
|
|
144
152
|
*/
|
|
145
|
-
corpusScope?:
|
|
153
|
+
corpusScope?: CorpusScope;
|
|
146
154
|
/**
|
|
147
155
|
* OPS/AUDIT escape hatch — do NOT use for normal search. Default behaviour
|
|
148
156
|
* (absent or false) excludes sampling-artifact products: multi-swatch sample
|
package/types.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import type { BySpecVerdict } from "./by-spec-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hand-mirrored wire types for the MaterialGraph public API (spec 0.5.0).
|
|
4
|
+
*
|
|
5
|
+
* These interfaces intentionally have NO runtime dependency on any
|
|
6
|
+
* `@materialgraph/*` package — they are a standalone, publishable mirror of the
|
|
7
|
+
* shapes in `apps/web/public/openapi.yaml` and the captured fixtures under
|
|
8
|
+
* `docs/surface/fixtures/v0.5.0/`. The `contract-parity` test re-checks them
|
|
9
|
+
* against the real Zod schemas so the mirror cannot silently drift.
|
|
10
|
+
* TOLERANT-PARSING CONTRACT: the public surface declares open response enums —
|
|
11
|
+
* "clients must tolerate unknown enum values and unknown fields". Every union
|
|
12
|
+
* that mirrors a server enum is therefore widened with `(string & {})` so a new
|
|
13
|
+
* server value still type-checks, and objects that may grow fields are read
|
|
14
|
+
* structurally (unknown keys pass through untouched — the client never strips).
|
|
15
|
+
*
|
|
16
|
+
* READING CONTRACT — the three things callers get wrong, stated once here and
|
|
17
|
+
* repeated on the types where each one bites:
|
|
18
|
+
* 1. **Ids are opaque.** Round-trip them verbatim; never parse or construct
|
|
19
|
+
* one. See `./identifiers.ts` for which strings are opaque tokens and which
|
|
20
|
+
* are canonical vocabulary you discover.
|
|
21
|
+
* 2. **Absent is not false.** An optional field that is missing means UNKNOWN,
|
|
22
|
+
* not "known to be absent in reality" — most dangerously for booleans.
|
|
23
|
+
* Values carry `confidence` and `sourceBasis`; report them as claims.
|
|
24
|
+
* 3. **A page is not a corpus.** `results.length` is this page.
|
|
25
|
+
* {@link CountedTotal} — with the `basis` it states — is the claim about the
|
|
26
|
+
* match set. Follow the cursor for the rest, and never report a page size as
|
|
27
|
+
* a total.
|
|
28
|
+
*/
|
|
29
|
+
import type { CorpusScopeReceipt } from "./corpus-scope-types.js";
|
|
2
30
|
import type { Cursor, ProductId, VariantId } from "./identifiers.js";
|
|
3
31
|
import type { MaterialDetail, MaterialTile } from "./material-types.js";
|
|
4
32
|
import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
|
|
@@ -388,11 +416,7 @@ export interface SearchResponse {
|
|
|
388
416
|
* MaterialGraph knows, orderable or not. Read it alongside `counts`: a short
|
|
389
417
|
* page under a scope is a small SCOPED corpus, not a truncated one.
|
|
390
418
|
*/
|
|
391
|
-
corpusScope?:
|
|
392
|
-
scope: OpenEnum<"all" | "materialBankOrderable">;
|
|
393
|
-
/** What the scope admits, in one sentence, for a human reader. */
|
|
394
|
-
description: string;
|
|
395
|
-
};
|
|
419
|
+
corpusScope?: CorpusScopeReceipt;
|
|
396
420
|
/** Echo of the resolved query input (raw image bytes/vector are never echoed). */
|
|
397
421
|
query?: Record<string, unknown>;
|
|
398
422
|
/** A `getVariantBrief` echo of the seed variant, present only in `variantId` seed mode. */
|