@instruments/materialgraph 0.3.0 → 0.5.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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The `browse` namespace's implementation — the paged colour-ordered walk of the
3
+ * corpus behind `client.browse(...)` and `client.browse.pages(...)`.
4
+ *
5
+ * Lifted out of `client.ts` for the repository's source-file size budget. These
6
+ * were already private implementations behind a public namespace, so nothing
7
+ * about the client's surface changes: `client.browse` is still a callable with a
8
+ * `.pages()` generator, and the class still owns the transport. What moved is
9
+ * the pagination LOOP, which is the only real logic here and the only part worth
10
+ * testing on its own.
11
+ */
12
+ import type { BrowseParams, BrowseResponse } from "./types.js";
13
+ /** `client.browse(...)` — callable for one page, `.pages()` for the whole walk. */
14
+ export interface BrowseNamespace {
15
+ (params: BrowseParams, signal?: AbortSignal): Promise<BrowseResponse>;
16
+ pages(params: BrowseParams, signal?: AbortSignal): AsyncGenerator<BrowseResponse, void, unknown>;
17
+ }
18
+ /** Fetch one page. The client supplies this, so the transport stays private. */
19
+ export type BrowsePageFetcher = (params: BrowseParams, signal?: AbortSignal) => Promise<BrowseResponse>;
20
+ /**
21
+ * The query string for one browse page — the four params the route reads, named
22
+ * here so the client method and the pagination loop cannot disagree about them.
23
+ */
24
+ export declare function browseQuery(params: BrowseParams): {
25
+ sort: BrowseParams["sort"];
26
+ cursor: BrowseParams["cursor"];
27
+ limit: BrowseParams["limit"];
28
+ types: BrowseParams["types"];
29
+ };
30
+ /**
31
+ * Walk every page from `params.cursor` onward.
32
+ *
33
+ * Stops on the first page that reports no next cursor OR comes back empty. Both
34
+ * conditions matter: a server that keeps handing out a cursor for an exhausted
35
+ * scan would otherwise spin here forever, and an infinite generator is a hang,
36
+ * not an error anyone can see.
37
+ */
38
+ export declare function browsePages(fetchPage: BrowsePageFetcher, params: BrowseParams, signal?: AbortSignal): AsyncGenerator<BrowseResponse, void, unknown>;
39
+ /**
40
+ * Build the callable-with-`.pages()` object the client exposes as `browse`.
41
+ *
42
+ * @example
43
+ * const page = await client.browse({ sort: "lightest" });
44
+ * for await (const page of client.browse.pages({ sort: "lightest" })) { … }
45
+ */
46
+ export declare function createBrowseNamespace(fetchPage: BrowsePageFetcher): BrowseNamespace;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The query string for one browse page — the four params the route reads, named
3
+ * here so the client method and the pagination loop cannot disagree about them.
4
+ */
5
+ export function browseQuery(params) {
6
+ return {
7
+ sort: params.sort,
8
+ cursor: params.cursor,
9
+ limit: params.limit,
10
+ types: params.types,
11
+ };
12
+ }
13
+ /**
14
+ * Walk every page from `params.cursor` onward.
15
+ *
16
+ * Stops on the first page that reports no next cursor OR comes back empty. Both
17
+ * conditions matter: a server that keeps handing out a cursor for an exhausted
18
+ * scan would otherwise spin here forever, and an infinite generator is a hang,
19
+ * not an error anyone can see.
20
+ */
21
+ export async function* browsePages(fetchPage, params, signal) {
22
+ let cursor = params.cursor;
23
+ for (;;) {
24
+ const page = await fetchPage({ ...params, cursor }, signal);
25
+ yield page;
26
+ if (page.cursor === undefined || page.results.length === 0)
27
+ return;
28
+ cursor = page.cursor;
29
+ }
30
+ }
31
+ /**
32
+ * Build the callable-with-`.pages()` object the client exposes as `browse`.
33
+ *
34
+ * @example
35
+ * const page = await client.browse({ sort: "lightest" });
36
+ * for await (const page of client.browse.pages({ sort: "lightest" })) { … }
37
+ */
38
+ export function createBrowseNamespace(fetchPage) {
39
+ const browse = (params, signal) => fetchPage(params, signal);
40
+ browse.pages = (params, signal) => browsePages(fetchPage, params, signal);
41
+ return browse;
42
+ }
package/client.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type RateLimit } from "./errors.js";
2
+ import { type BrowseNamespace } from "./client-browse.js";
3
+ export type { BrowseNamespace } from "./client-browse.js";
2
4
  import type { CapabilityManifestResponse, CreatePublicRunRequest, CreatePublicRunResponse, DrainRunEventsOptions, PublicRunResponse, ResumeRunOptions, RunEvent, SourceMaterialBatchDetail, SourceMaterialBatchReceipt, StreamRunEventsOptions, UploadSourceMaterialInput } from "./run-types.js";
3
5
  import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
4
6
  import type { MatchStreamEvent } from "./match-stream-types.js";
@@ -11,7 +13,7 @@ import type { BrandCatalogListPage, BrandsOptions } from "./brand-types.js";
11
13
  import type { MaterialAssetProjection } from "./material-asset-types.js";
12
14
  import type { MaterialBlockProjection } from "./material-block-types.js";
13
15
  import type { SchemaResource, SchemaResourceResponse } from "./schema-resource-types.js";
14
- import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, ByColourParams, ByColourResponse, BrowseParams, BrowseResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandBatchInput, DemandBatchResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, MatchInput, MatchResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, TaxonomyResponse, VocabularySearchOptions, VocabularySearchResponse, WhoamiResponse } from "./types.js";
16
+ import type { ApplicableSpecsResponse, BrandOptions, BrandProfileResponse, ByColourParams, ByColourResponse, BySpecOptions, BySpecResponse, ColourFacetsResponse, CoordinateInput, CoordinateResponse, DemandBatchInput, DemandBatchResponse, DemandParams, DemandResponse, ExtractPaletteInput, ExtractPaletteResponse, GlossaryTermResponse, MaterialResponse, MaterialsBatchDetailResponse, MaterialsBatchGetOptions, MaterialsBatchOptions, MaterialsBatchTileResponse, MatchInput, MatchResponse, PairingsOptions, PairingsResponse, ResolveAttributeOptions, ResolveAttributeResponse, ResolveAttributeValueOptions, ResolveAttributeValueResponse, ResolveColourResponse, SearchGetParams, SearchInput, SearchResponse, SiblingsResponse, SpecFacetsDistributionResponse, SpecFacetsListResponse, SuggestColourResponse, SuggestOptions, SuggestResponse, TaxonomyResponse, VocabularySearchOptions, VocabularySearchResponse, WhoamiResponse } from "./types.js";
15
17
  export declare const DEFAULT_BASE_URL = "https://beta.materialgraph.com";
16
18
  export declare const MAX_BATCH_IDS = 100;
17
19
  export interface MaterialGraphClientOptions {
@@ -22,10 +24,6 @@ export interface MaterialGraphClientOptions {
22
24
  /** Inject a custom `fetch` (tests, proxies). Defaults to the global `fetch`. */
23
25
  fetch?: typeof fetch;
24
26
  }
25
- export interface BrowseNamespace {
26
- (params: BrowseParams, signal?: AbortSignal): Promise<BrowseResponse>;
27
- pages(params: BrowseParams, signal?: AbortSignal): AsyncGenerator<BrowseResponse, void, unknown>;
28
- }
29
27
  export declare class MaterialGraphClient {
30
28
  #private;
31
29
  readonly baseUrl: string;
@@ -130,6 +128,18 @@ export declare class MaterialGraphClient {
130
128
  suggestColour(q: string, options?: {
131
129
  limit?: number;
132
130
  }, signal?: AbortSignal): Promise<SuggestColourResponse>;
131
+ /**
132
+ * Entity typeahead across products, brands and categories
133
+ * (GET `/api/v1/search/suggest`). Built for per-keystroke use: one indexed
134
+ * lookup, no embedding call and no model. Every suggestion carries the
135
+ * `refId` it resolves to — a product id, a brand slug, or a product-type key
136
+ * — so selecting one should navigate to that entity rather than re-running
137
+ * `label` as a search.
138
+ *
139
+ * @example
140
+ * const { suggestions } = await client.suggest("carr", { kind: "product" });
141
+ */
142
+ suggest(q: string, options?: SuggestOptions, signal?: AbortSignal): Promise<SuggestResponse>;
133
143
  /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
134
144
  resolveAttribute(q: string, options?: ResolveAttributeOptions, signal?: AbortSignal): Promise<ResolveAttributeResponse>;
135
145
  /** Resolve a value phrase against one canonical attribute's dictionary. */
package/client.js CHANGED
@@ -2,6 +2,8 @@ import { MaterialGraphError } from "./errors.js";
2
2
  import { HttpTransport } from "./http-transport.js";
3
3
  import { findSdkOperation } from "./operations.js";
4
4
  import { cancelRun, capabilities as capabilitiesRun, createRun, drainRunEvents, getRun, getRunSchedule, getSourceMaterialBatch as getSourceMaterialBatchCall, resumeRun, streamRunEvents, uploadSourceMaterial, } from "./runs.js";
5
+ import { browseQuery, createBrowseNamespace } from "./client-browse.js";
6
+ import { suggestQuery } from "./suggest-types.js";
5
7
  import { matchStream as matchStreamCall } from "./match-stream.js";
6
8
  import { appendProjectEvent as appendProjectEventCall, createProject as createProjectCall, getProject as getProjectCall, listProjects as listProjectsCall, } from "./projects.js";
7
9
  import { CodesClient } from "./codes-client.js";
@@ -51,15 +53,14 @@ export class MaterialGraphClient {
51
53
  fetch: options.fetch ?? globalThis.fetch,
52
54
  });
53
55
  this.baseUrl = this.#transport.baseUrl;
54
- const browse = (params, signal) => this.#browsePage(params, signal);
55
- browse.pages = (params, signal) => this.#browsePages(params, signal);
56
- this.browse = browse;
56
+ this.browse = createBrowseNamespace((params, signal) => this.#get("search_browse_by_colour", browseQuery(params), signal));
57
57
  this.codes = new CodesClient({
58
58
  get: (operationId, query, signal) => this.#get(operationId, query, signal),
59
59
  post: (operationId, body, signal) => this.#post(operationId, body, signal),
60
60
  });
61
61
  this.colours = new ColoursClient({
62
62
  get: (operationId, query, signal) => this.#get(operationId, query, signal),
63
+ post: (operationId, body, signal) => this.#post(operationId, body, signal),
63
64
  });
64
65
  }
65
66
  get lastRateLimit() {
@@ -173,25 +174,6 @@ export class MaterialGraphClient {
173
174
  idKind: options?.idKind ?? "variantId",
174
175
  }, signal);
175
176
  }
176
- // ── Browse (private impls behind the `browse` namespace) ─────────────────────
177
- #browsePage(params, signal) {
178
- return this.#get("search_browse_by_colour", {
179
- sort: params.sort,
180
- cursor: params.cursor,
181
- limit: params.limit,
182
- types: params.types,
183
- }, signal);
184
- }
185
- async *#browsePages(params, signal) {
186
- let cursor = params.cursor;
187
- for (;;) {
188
- const page = await this.#browsePage({ ...params, cursor }, signal);
189
- yield page;
190
- if (page.cursor === undefined || page.results.length === 0)
191
- return;
192
- cursor = page.cursor;
193
- }
194
- }
195
177
  // ── Colour helpers ───────────────────────────────────────────────────────────
196
178
  /** The three canonical colour vocabularies with live counts (GET `/api/v1/colours/facets`). */
197
179
  colourFacets(signal) {
@@ -205,6 +187,20 @@ export class MaterialGraphClient {
205
187
  suggestColour(q, options, signal) {
206
188
  return this.#get("suggest_colour_names", { q, limit: options?.limit }, signal);
207
189
  }
190
+ /**
191
+ * Entity typeahead across products, brands and categories
192
+ * (GET `/api/v1/search/suggest`). Built for per-keystroke use: one indexed
193
+ * lookup, no embedding call and no model. Every suggestion carries the
194
+ * `refId` it resolves to — a product id, a brand slug, or a product-type key
195
+ * — so selecting one should navigate to that entity rather than re-running
196
+ * `label` as a search.
197
+ *
198
+ * @example
199
+ * const { suggestions } = await client.suggest("carr", { kind: "product" });
200
+ */
201
+ suggest(q, options, signal) {
202
+ return this.#get("search_suggest", suggestQuery(q, options), signal);
203
+ }
208
204
  // ── Catalog ──────────────────────────────────────────────────────────────────
209
205
  /** Resolve a free-text spec phrase to canonical attribute ids (GET `/api/v1/catalog/attributes/resolve`). */
210
206
  resolveAttribute(q, options, signal) {
@@ -1,26 +1,31 @@
1
1
  /**
2
- * The colour AGGREGATE surface, namespaced as `mg.colours`.
2
+ * The colour surface, namespaced as `mg.colours` — the aggregate reads
3
+ * (`stats`, `profiles`) plus MULTI-COLOUR search (`palette`, `gradient`).
3
4
  *
4
- * Two reads that answer the same question at two altitudes — "how much colour
5
- * does the graph hold at all" (`stats`) and "what colours does this category
6
- * come in" (`profiles`) — so they live together rather than as two more flat
7
- * methods on an already-large client facade. `mg.codes` and `mg.browse` set the
8
- * precedent for a namespace when a capability is a small family rather than a
9
- * single verb.
5
+ * One colour family rather than four more flat methods on an already-large
6
+ * client facade: "how much colour does the graph hold at all" (`stats`), "what
7
+ * colours does this category come in" (`profiles`), "what goes with these
8
+ * colours" (`palette`), and "what moves from this colour to that one"
9
+ * (`gradient`). `mg.codes` and `mg.browse` set the precedent for a namespace
10
+ * when a capability is a family rather than a single verb.
10
11
  *
11
12
  * The namespace holds no transport of its own: the client hands it a bound
12
- * `get`, so auth, rate-limit accounting and error shaping stay in one place.
13
+ * `get`/`post`, so auth, rate-limit accounting and error shaping stay in one
14
+ * place.
13
15
  *
14
- * Both responses carry a `basis`. These are reads of a materialized view that is
15
- * refreshed on a schedule, not on write, so a caller can and should check how
16
- * much of the corpus the answer actually covers before treating it as complete.
16
+ * The two AGGREGATE responses carry a `basis`. Those are reads of a
17
+ * materialized view refreshed on a schedule, not on write, so a caller can and
18
+ * should check how much of the corpus the answer actually covers before
19
+ * treating it as complete. The two SEARCH responses are live and uncached.
17
20
  */
18
21
  import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsResponse } from "./colour-aggregate-types.js";
19
22
  import type { QueryParams } from "./http-transport.js";
20
23
  import type { SdkOperationId } from "./operations.js";
21
- /** The client's own request primitive, handed down so this file owns no transport. */
24
+ import type { SearchGradientInput, SearchGradientResponse, SearchPaletteInput, SearchPaletteResponse } from "./palette-search-types.js";
25
+ /** The client's own request primitives, handed down so this file owns no transport. */
22
26
  export interface ColoursTransport {
23
27
  get<T>(operationId: SdkOperationId, query?: QueryParams, signal?: AbortSignal): Promise<T>;
28
+ post<T>(operationId: SdkOperationId, body: unknown, signal?: AbortSignal): Promise<T>;
24
29
  }
25
30
  export declare class ColoursClient {
26
31
  #private;
@@ -49,4 +54,42 @@ export declare class ColoursClient {
49
54
  * answer, listed rather than silently absent.
50
55
  */
51
56
  stats(signal?: AbortSignal): Promise<ColourStatsResponse>;
57
+ /**
58
+ * Find materials that go with a WHOLE PALETTE — N colours at once, not one
59
+ * colour repeated (POST `/api/v1/search/palettes`).
60
+ *
61
+ * Each colour scores `sim × prop`: `sim` is OKLab closeness on the same
62
+ * mapping `byColour` ranks with, and `prop` rewards a candidate whose own
63
+ * swatch SHARE sits near the `proportion` you asked for. So the same colours
64
+ * at different proportions come back in a different order — a colour at 60%
65
+ * of the surface is a different query from one at 5%. `logic: "all"` (the
66
+ * default) applies a per-colour eligibility floor and returns the weighted
67
+ * geometric mean, so a candidate missing one colour is rejected rather than
68
+ * merely ranked low; `logic: "any"` is a noisy-OR that accepts partial
69
+ * answers but saturates once several colours score well, so prefer `all`
70
+ * when the ORDER is what matters.
71
+ *
72
+ * Every match carries the candidate's full `palette` plus a per-colour
73
+ * receipt naming the swatch that answered each queried colour.
74
+ *
75
+ * @example
76
+ * const { matches } = await mg.colours.palette({
77
+ * colours: [
78
+ * { hex: "F5EFE0", proportion: 0.6 },
79
+ * { hex: "9CAF88", proportion: 0.2 },
80
+ * { hex: "3E3B36", proportion: 0.2 },
81
+ * ],
82
+ * });
83
+ */
84
+ palette(input: SearchPaletteInput, signal?: AbortSignal): Promise<SearchPaletteResponse>;
85
+ /**
86
+ * Find materials that move ALONG a colour transition — "cream to rust", in
87
+ * order (POST `/api/v1/search/gradients`).
88
+ *
89
+ * Not a palette: the score is multiplied by `coverage`, the share of the path
90
+ * the candidate actually spans, so a material owning only one end ranks below
91
+ * one that travels the whole way, and a flat single-colour material is
92
+ * rejected outright however close its hue sits to the path.
93
+ */
94
+ gradient(input: SearchGradientInput, signal?: AbortSignal): Promise<SearchGradientResponse>;
52
95
  }
package/colours-client.js CHANGED
@@ -36,4 +36,46 @@ export class ColoursClient {
36
36
  stats(signal) {
37
37
  return this.#transport.get("get_colour_stats", undefined, signal);
38
38
  }
39
+ /**
40
+ * Find materials that go with a WHOLE PALETTE — N colours at once, not one
41
+ * colour repeated (POST `/api/v1/search/palettes`).
42
+ *
43
+ * Each colour scores `sim × prop`: `sim` is OKLab closeness on the same
44
+ * mapping `byColour` ranks with, and `prop` rewards a candidate whose own
45
+ * swatch SHARE sits near the `proportion` you asked for. So the same colours
46
+ * at different proportions come back in a different order — a colour at 60%
47
+ * of the surface is a different query from one at 5%. `logic: "all"` (the
48
+ * default) applies a per-colour eligibility floor and returns the weighted
49
+ * geometric mean, so a candidate missing one colour is rejected rather than
50
+ * merely ranked low; `logic: "any"` is a noisy-OR that accepts partial
51
+ * answers but saturates once several colours score well, so prefer `all`
52
+ * when the ORDER is what matters.
53
+ *
54
+ * Every match carries the candidate's full `palette` plus a per-colour
55
+ * receipt naming the swatch that answered each queried colour.
56
+ *
57
+ * @example
58
+ * const { matches } = await mg.colours.palette({
59
+ * colours: [
60
+ * { hex: "F5EFE0", proportion: 0.6 },
61
+ * { hex: "9CAF88", proportion: 0.2 },
62
+ * { hex: "3E3B36", proportion: 0.2 },
63
+ * ],
64
+ * });
65
+ */
66
+ palette(input, signal) {
67
+ return this.#transport.post("search_by_palette", input, signal);
68
+ }
69
+ /**
70
+ * Find materials that move ALONG a colour transition — "cream to rust", in
71
+ * order (POST `/api/v1/search/gradients`).
72
+ *
73
+ * Not a palette: the score is multiplied by `coverage`, the share of the path
74
+ * the candidate actually spans, so a material owning only one end ranks below
75
+ * one that travels the whole way, and a flat single-colour material is
76
+ * rejected outright however close its hue sits to the path.
77
+ */
78
+ gradient(input, signal) {
79
+ return this.#transport.post("search_by_gradient", input, signal);
80
+ }
39
81
  }
package/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export type * from "./codes-types.js";
21
21
  export { ColoursClient } from "./colours-client.js";
22
22
  export type { ColoursTransport } from "./colours-client.js";
23
23
  export type * from "./colour-aggregate-types.js";
24
+ export type * from "./palette-search-types.js";
24
25
  export type * from "./brand-types.js";
25
26
  export type * from "./material-asset-types.js";
26
27
  export type * from "./material-block-types.js";
@@ -1,6 +1,6 @@
1
1
  import type { OpenEnum } from "./types.js";
2
2
  export type MaterialAssetVisualRole = OpenEnum<"grid" | "detail" | "scene" | "swatch" | "technical" | "unknown">;
3
- export type MaterialAssetStatus = OpenEnum<"downloaded" | "linked_not_downloaded" | "failed">;
3
+ export type MaterialAssetStatus = OpenEnum<"downloaded" | "linked_not_downloaded" | "failed" | "retired_placeholder">;
4
4
  export interface MaterialAssetProjectionImage {
5
5
  key: string;
6
6
  ordinal: number;
package/operations.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const MATERIALGRAPH_API_VERSION = "0.7.0";
2
- declare const OPERATION_DEFINITIONS: readonly [readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["searchGet", "GET", "/api/v1/search", "search_materials_get"], readonly ["match", "POST", "/api/v1/search/match", "match_material"], readonly ["byColour", "GET", "/api/v1/search/colours", "search_by_colour"], readonly ["material", "GET", "/api/v1/materials/{variantId}", "get_material"], readonly ["siblings", "GET", "/api/v1/materials/{variantId}/siblings", "get_material_siblings"], readonly ["materialBlocks", "GET", "/api/v1/materials/{variantId}/blocks", "list_material_blocks"], readonly ["materialAssets", "GET", "/api/v1/materials/{variantId}/assets", "list_material_assets"], readonly ["schema", "GET", "/api/v1/schema/blocks", "list_schema_blocks"], readonly ["schema", "GET", "/api/v1/schema/attributes", "list_schema_attributes"], readonly ["schema", "GET", "/api/v1/schema/verticals", "list_schema_verticals"], readonly ["schema", "GET", "/api/v1/schema/product-types", "list_schema_product_types"], readonly ["schema", "GET", "/api/v1/schema/units", "list_schema_units"], readonly ["schema", "GET", "/api/v1/schema/value-dictionaries", "list_schema_value_dictionaries"], readonly ["schema", "GET", "/api/v1/schema/source-requirements", "list_schema_source_requirements"], readonly ["schema", "GET", "/api/v1/schema/product-type-templates", "list_schema_product_type_templates"], readonly ["schema", "GET", "/api/v1/schema/mappings", "list_schema_mappings"], readonly ["pairings", "GET", "/api/v1/materials/{variantId}/pairings", "get_material_pairings"], readonly ["materials", "POST", "/api/v1/materials/batch", "get_materials_batch_post"], readonly ["materialsCached", "GET", "/api/v1/materials/batch", "get_materials_batch_get"], readonly ["browse", "GET", "/api/v1/search/browse", "search_browse_by_colour"], readonly ["colourFacets", "GET", "/api/v1/colours/facets", "get_colour_facets"], readonly ["colours.profiles", "GET", "/api/v1/colours/profiles", "get_category_colour_profiles"], readonly ["colours.stats", "GET", "/api/v1/colours/stats", "get_colour_stats"], readonly ["resolveColour", "GET", "/api/v1/search/colours/resolve", "resolve_colour_name"], readonly ["suggestColour", "GET", "/api/v1/search/colours/suggest", "suggest_colour_names"], readonly ["resolveAttribute", "GET", "/api/v1/catalog/attributes/resolve", "catalog_resolve_attribute"], readonly ["resolveAttributeValue", "GET", "/api/v1/catalog/attributes/{attributeId}/values/resolve", "catalog_resolve_value"], readonly ["searchVocabulary", "GET", "/api/v1/catalog/vocabulary/search", "catalog_vocabulary_search"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["whoami", "GET", "/api/v1/whoami", "whoami"], readonly ["coordinate", "POST", "/api/v1/pairings/coordinate", "coordinate_materials"], readonly ["demand", "GET", "/api/v1/demand", "get_material_demand"], readonly ["extractPalette", "POST", "/api/v1/search/palettes/extract", "extract_image_palette"], readonly ["specificationFacets", "GET", "/api/v1/catalog/specifications/facets", "list_spec_facets"], readonly ["attributeValues", "GET", "/api/v1/catalog/attributes/{attributeId}/values", "list_attribute_values"], readonly ["applicableSpecs", "GET", "/api/v1/taxonomy/categories/{key}/specs", "get_applicable_specs"], readonly ["glossary", "GET", "/api/v1/glossary/{term}", "explain_material_term"], readonly ["brands", "GET", "/api/v1/brands", "list_brands"], readonly ["brand", "GET", "/api/v1/brands/{brandSlug}", "get_brand_profile"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["streamRunEvents", "GET", "/api/v1/runs/{runId}/events", "list_public_run_events"], readonly ["resumeRun", "POST", "/api/v1/runs/{runId}/resume", "resume_public_run"], readonly ["cancelRun", "POST", "/api/v1/runs/{runId}/cancel", "cancel_public_run"], readonly ["varyScheme", "POST", "/api/v1/schemes/vary", "vary_schemes"], readonly ["createProject", "POST", "/api/v1/projects", "create_box_project"], readonly ["listProjects", "GET", "/api/v1/projects", "list_box_projects"], readonly ["getProject", "GET", "/api/v1/projects/{projectId}", "get_box_project"], readonly ["appendProjectEvent", "POST", "/api/v1/projects/{projectId}/events", "append_box_project_event"], readonly ["demandBatch", "POST", "/api/v1/demand/batch", "get_demand_batch"], readonly ["codeAdvisory", "POST", "/api/v1/codes/advisory", "resolveCodeAdvisory"], readonly ["codes.context", "GET", "/api/v1/codes/context", "list_code_context"], readonly ["codes.requirements", "GET", "/api/v1/codes/requirements", "list_code_requirements"]];
2
+ declare const OPERATION_DEFINITIONS: readonly [readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["searchGet", "GET", "/api/v1/search", "search_materials_get"], readonly ["match", "POST", "/api/v1/search/match", "match_material"], readonly ["byColour", "GET", "/api/v1/search/colours", "search_by_colour"], readonly ["material", "GET", "/api/v1/materials/{variantId}", "get_material"], readonly ["siblings", "GET", "/api/v1/materials/{variantId}/siblings", "get_material_siblings"], readonly ["materialBlocks", "GET", "/api/v1/materials/{variantId}/blocks", "list_material_blocks"], readonly ["materialAssets", "GET", "/api/v1/materials/{variantId}/assets", "list_material_assets"], readonly ["schema", "GET", "/api/v1/schema/blocks", "list_schema_blocks"], readonly ["schema", "GET", "/api/v1/schema/attributes", "list_schema_attributes"], readonly ["schema", "GET", "/api/v1/schema/verticals", "list_schema_verticals"], readonly ["schema", "GET", "/api/v1/schema/product-types", "list_schema_product_types"], readonly ["schema", "GET", "/api/v1/schema/units", "list_schema_units"], readonly ["schema", "GET", "/api/v1/schema/value-dictionaries", "list_schema_value_dictionaries"], readonly ["schema", "GET", "/api/v1/schema/source-requirements", "list_schema_source_requirements"], readonly ["schema", "GET", "/api/v1/schema/product-type-templates", "list_schema_product_type_templates"], readonly ["schema", "GET", "/api/v1/schema/mappings", "list_schema_mappings"], readonly ["pairings", "GET", "/api/v1/materials/{variantId}/pairings", "get_material_pairings"], readonly ["materials", "POST", "/api/v1/materials/batch", "get_materials_batch_post"], readonly ["materialsCached", "GET", "/api/v1/materials/batch", "get_materials_batch_get"], readonly ["browse", "GET", "/api/v1/search/browse", "search_browse_by_colour"], readonly ["colourFacets", "GET", "/api/v1/colours/facets", "get_colour_facets"], readonly ["colours.profiles", "GET", "/api/v1/colours/profiles", "get_category_colour_profiles"], readonly ["colours.stats", "GET", "/api/v1/colours/stats", "get_colour_stats"], readonly ["resolveColour", "GET", "/api/v1/search/colours/resolve", "resolve_colour_name"], readonly ["suggestColour", "GET", "/api/v1/search/colours/suggest", "suggest_colour_names"], readonly ["suggest", "GET", "/api/v1/search/suggest", "search_suggest"], readonly ["resolveAttribute", "GET", "/api/v1/catalog/attributes/resolve", "catalog_resolve_attribute"], readonly ["resolveAttributeValue", "GET", "/api/v1/catalog/attributes/{attributeId}/values/resolve", "catalog_resolve_value"], readonly ["searchVocabulary", "GET", "/api/v1/catalog/vocabulary/search", "catalog_vocabulary_search"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["whoami", "GET", "/api/v1/whoami", "whoami"], readonly ["coordinate", "POST", "/api/v1/pairings/coordinate", "coordinate_materials"], readonly ["demand", "GET", "/api/v1/demand", "get_material_demand"], readonly ["colours.palette", "POST", "/api/v1/search/palettes", "search_by_palette"], readonly ["extractPalette", "POST", "/api/v1/search/palettes/extract", "extract_image_palette"], readonly ["colours.gradient", "POST", "/api/v1/search/gradients", "search_by_gradient"], readonly ["specificationFacets", "GET", "/api/v1/catalog/specifications/facets", "list_spec_facets"], readonly ["attributeValues", "GET", "/api/v1/catalog/attributes/{attributeId}/values", "list_attribute_values"], readonly ["applicableSpecs", "GET", "/api/v1/taxonomy/categories/{key}/specs", "get_applicable_specs"], readonly ["glossary", "GET", "/api/v1/glossary/{term}", "explain_material_term"], readonly ["brands", "GET", "/api/v1/brands", "list_brands"], readonly ["brand", "GET", "/api/v1/brands/{brandSlug}", "get_brand_profile"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["streamRunEvents", "GET", "/api/v1/runs/{runId}/events", "list_public_run_events"], readonly ["resumeRun", "POST", "/api/v1/runs/{runId}/resume", "resume_public_run"], readonly ["cancelRun", "POST", "/api/v1/runs/{runId}/cancel", "cancel_public_run"], readonly ["varyScheme", "POST", "/api/v1/schemes/vary", "vary_schemes"], readonly ["createProject", "POST", "/api/v1/projects", "create_box_project"], readonly ["listProjects", "GET", "/api/v1/projects", "list_box_projects"], readonly ["getProject", "GET", "/api/v1/projects/{projectId}", "get_box_project"], readonly ["appendProjectEvent", "POST", "/api/v1/projects/{projectId}/events", "append_box_project_event"], readonly ["demandBatch", "POST", "/api/v1/demand/batch", "get_demand_batch"], readonly ["codeAdvisory", "POST", "/api/v1/codes/advisory", "resolveCodeAdvisory"], readonly ["codes.context", "GET", "/api/v1/codes/context", "list_code_context"], readonly ["codes.requirements", "GET", "/api/v1/codes/requirements", "list_code_requirements"]];
3
3
  export type SdkOperationId = (typeof OPERATION_DEFINITIONS)[number][3];
4
4
  export type SdkOperation = Readonly<{
5
5
  clientMethod: (typeof OPERATION_DEFINITIONS)[number][0];
package/operations.js CHANGED
@@ -26,6 +26,7 @@ const OPERATION_DEFINITIONS = [
26
26
  ["colours.stats", "GET", "/api/v1/colours/stats", "get_colour_stats"],
27
27
  ["resolveColour", "GET", "/api/v1/search/colours/resolve", "resolve_colour_name"],
28
28
  ["suggestColour", "GET", "/api/v1/search/colours/suggest", "suggest_colour_names"],
29
+ ["suggest", "GET", "/api/v1/search/suggest", "search_suggest"],
29
30
  ["resolveAttribute", "GET", "/api/v1/catalog/attributes/resolve", "catalog_resolve_attribute"],
30
31
  [
31
32
  "resolveAttributeValue",
@@ -39,7 +40,9 @@ const OPERATION_DEFINITIONS = [
39
40
  ["whoami", "GET", "/api/v1/whoami", "whoami"],
40
41
  ["coordinate", "POST", "/api/v1/pairings/coordinate", "coordinate_materials"],
41
42
  ["demand", "GET", "/api/v1/demand", "get_material_demand"],
43
+ ["colours.palette", "POST", "/api/v1/search/palettes", "search_by_palette"],
42
44
  ["extractPalette", "POST", "/api/v1/search/palettes/extract", "extract_image_palette"],
45
+ ["colours.gradient", "POST", "/api/v1/search/gradients", "search_by_gradient"],
43
46
  ["specificationFacets", "GET", "/api/v1/catalog/specifications/facets", "list_spec_facets"],
44
47
  [
45
48
  "attributeValues",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Wire types for MULTI-COLOUR search — `POST /api/v1/search/palettes` and
3
+ * `POST /api/v1/search/gradients`.
4
+ *
5
+ * Kept out of `types.ts` for the same reason `colour-aggregate-types.ts` and
6
+ * `codes-types.ts` are: the shared mirror is at its source-size budget. Same
7
+ * tolerant-parsing contract as the rest of the mirror — response enums are
8
+ * widened so a new server value still type-checks.
9
+ *
10
+ * The MATCH SEMANTICS these types carry are the server's, stated on
11
+ * {@link SearchPaletteInput.logic} and {@link GradientMatch.coverage} rather
12
+ * than left to the endpoint docs.
13
+ */
14
+ import type { OklabTriple, OpenEnum } from "./types.js";
15
+ /** How the per-colour scores are combined across a palette. */
16
+ export type PaletteLogic = OpenEnum<"all" | "any">;
17
+ /** One queried palette colour — exactly one of `hex` or `oklab`. */
18
+ export interface PaletteQueryColour {
19
+ hex?: string;
20
+ oklab?: OklabTriple;
21
+ /**
22
+ * Share of the surface this colour should occupy, in (0,1]. Stating it
23
+ * CHANGES the ranking: a colour asked for at 0.6 rewards candidates whose own
24
+ * swatch sits near 0.6, not merely candidates that contain the colour.
25
+ */
26
+ proportion?: number;
27
+ /** Relative importance; defaults to 1 and is normalised across the query. */
28
+ weight?: number;
29
+ }
30
+ /** One gradient stop — exactly one of `hex` or `oklab`. Order is meaningful. */
31
+ export interface GradientStop {
32
+ hex?: string;
33
+ oklab?: OklabTriple;
34
+ }
35
+ export interface SearchPaletteInput {
36
+ /** The palette to match, 1–8 colours. */
37
+ colours: PaletteQueryColour[];
38
+ /**
39
+ * `all` (the default) requires every queried colour to clear a per-colour
40
+ * eligibility floor and scores the weighted geometric mean; `any` is a
41
+ * noisy-OR and never rejects a candidate for missing one colour.
42
+ */
43
+ logic?: PaletteLogic;
44
+ /** Results to return; defaults to 24, capped at 48. */
45
+ limit?: number;
46
+ }
47
+ export interface SearchGradientInput {
48
+ /** The ordered colour transition, 2–6 stops. */
49
+ stops: GradientStop[];
50
+ /** Results to return; defaults to 24, capped at 48. */
51
+ limit?: number;
52
+ }
53
+ /** One swatch of a candidate's colorscope-extracted palette. */
54
+ export interface CandidateSwatch {
55
+ hex: string;
56
+ proportion: number;
57
+ name: string | null;
58
+ }
59
+ /** Why a queried colour (or gradient sample) scored what it scored. */
60
+ export interface ColourMatchReceipt {
61
+ queryHex: string;
62
+ /** `colourScore × proportionScore` for the best-matching swatch. */
63
+ score: number;
64
+ /** OKLab closeness alone, in [0,1]. */
65
+ colourScore: number;
66
+ /** Proportion agreement alone, in [0,1]. */
67
+ proportionScore: number;
68
+ /** The swatch that produced the score, or null when nothing matched. */
69
+ matched: CandidateSwatch | null;
70
+ }
71
+ /** Identity and evidence shared by both multi-colour match shapes. */
72
+ export interface MultiColourMatchBase {
73
+ variantId: string;
74
+ brand: string;
75
+ product: string;
76
+ brandSlug: string | null;
77
+ productId: string | null;
78
+ color: string | null;
79
+ variantName: string | null;
80
+ imageUrl: string | null;
81
+ /** MB has retired or hidden this SKU. */
82
+ retired: boolean;
83
+ /** The candidate's full extracted palette — the evidence scored against. */
84
+ palette: CandidateSwatch[];
85
+ /** The aggregate match score in [0,1]; higher is closer. */
86
+ score: number;
87
+ }
88
+ export interface PaletteMatch extends MultiColourMatchBase {
89
+ /** One receipt per queried colour, in query order. */
90
+ colours: ColourMatchReceipt[];
91
+ }
92
+ export interface GradientMatch extends MultiColourMatchBase {
93
+ /**
94
+ * Distinct matched thirds of the path over 3. A material owning only one end
95
+ * of the transition caps out around 0.33 — coverage is what separates a real
96
+ * gradient match from a good match on a single stop.
97
+ */
98
+ coverage: number;
99
+ /** One receipt per sampled point along the path, in path order. */
100
+ samples: ColourMatchReceipt[];
101
+ }
102
+ export interface SearchPaletteResponse {
103
+ query: {
104
+ colours: Array<{
105
+ hex: string;
106
+ proportion?: number;
107
+ weight?: number;
108
+ }>;
109
+ logic: PaletteLogic;
110
+ };
111
+ count: number;
112
+ matches: PaletteMatch[];
113
+ }
114
+ export interface SearchGradientResponse {
115
+ query: {
116
+ stops: string[];
117
+ /** The sampled path points the search actually ran against. */
118
+ samples: string[];
119
+ };
120
+ count: number;
121
+ matches: GradientMatch[];
122
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ };
@@ -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
@@ -363,6 +363,7 @@ export interface ResolveAttributeResponse {
363
363
  candidates: AttributeCandidate[];
364
364
  }
365
365
  export type * from "./catalog-discovery-types.js";
366
+ export type * from "./suggest-types.js";
366
367
  export type * from "./by-spec-types.js";
367
368
  export interface WhoamiResponse {
368
369
  principal: string;