@instruments/materialgraph 0.19.0 → 0.20.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,51 @@
1
+ /**
2
+ * Result attribution (MG-1131) — joining an outcome back to the ranking that
3
+ * produced it.
4
+ *
5
+ * When someone orders a sample days after a search, a list of SKUs cannot say
6
+ * which response returned it or on what basis it was ranked. Store
7
+ * {@link SearchResultItem.resultId} next to the order row and
8
+ * {@link ResponseAttribution} beside the search, and both questions become a
9
+ * join in your own warehouse.
10
+ *
11
+ * MaterialGraph keeps no ledger of these ids. What you store IS the record.
12
+ */
13
+ /**
14
+ * The ranking basis, in one vocabulary across both ranked routes.
15
+ *
16
+ * The first five values are `search()`'s `mode` verbatim, and there
17
+ * `attribution.basis === mode` always. `"match"` is what `match()` reports;
18
+ * that route has no `mode`, so this is the only field its outcomes can be
19
+ * grouped by.
20
+ */
21
+ export type AttributionBasis = "image" | "colour" | "text" | "lexical" | "entity-browse" | "match";
22
+ /**
23
+ * Envelope-level attribution receipt, on every successful ranked response.
24
+ *
25
+ * Store this beside the search and each result's `resultId` beside the outcome
26
+ * (an order, a save, a click), and "which sample was ordered off which ranking,
27
+ * on what basis" becomes a join in your own warehouse.
28
+ *
29
+ * A `resultId` is `<responseId>:<rank>`, rank being 1-based within the response
30
+ * that returned it — unique across pages, because each page is its own
31
+ * response. On the Match stream lane it also carries the surface id, so a hit
32
+ * sent twice (on its `surface` frame and again inside `complete`) keeps one key.
33
+ *
34
+ * Both ids are OPAQUE: store them verbatim, never parse them. Group on
35
+ * `responseId` and `basis`.
36
+ *
37
+ * Both fields are optional in the SDK types for one reason only: a server
38
+ * predating MG-1131 does not send them. Every current response does.
39
+ */
40
+ export interface ResponseAttribution {
41
+ /**
42
+ * This response's stable id — the same value as the `X-Request-Id` header.
43
+ * Opaque: store it verbatim, never parse it. Group your stored outcomes by
44
+ * this field to recover the response that produced them.
45
+ */
46
+ responseId: string;
47
+ /** How this ranking was produced. The field to group outcomes by. */
48
+ basis: AttributionBasis;
49
+ /** ISO-8601 timestamp of when the response was assembled. */
50
+ issuedAt: string;
51
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Result attribution (MG-1131) — joining an outcome back to the ranking that
3
+ * produced it.
4
+ *
5
+ * When someone orders a sample days after a search, a list of SKUs cannot say
6
+ * which response returned it or on what basis it was ranked. Store
7
+ * {@link SearchResultItem.resultId} next to the order row and
8
+ * {@link ResponseAttribution} beside the search, and both questions become a
9
+ * join in your own warehouse.
10
+ *
11
+ * MaterialGraph keeps no ledger of these ids. What you store IS the record.
12
+ */
13
+ export {};
@@ -99,6 +99,15 @@ export interface ColourFamilyGate {
99
99
  * once the page is cut to `limit`; this is the gate's own cost.
100
100
  */
101
101
  dropped: number;
102
+ /**
103
+ * Rows dropped a SECOND time, after the page was hydrated with the brand's
104
+ * declared `colour_hex`. A paint's chip photograph and its published hex are
105
+ * different colours and can fall in different families, so the page is
106
+ * re-gated on the hex it reports — every `hex` you receive belongs to the
107
+ * family you asked for. Absent means hydration moved nothing across a
108
+ * boundary.
109
+ */
110
+ droppedOnPublishedHex?: number;
102
111
  }
103
112
  /** Receipt for the hard category gate and its treatment of unknown typing. */
104
113
  export interface ColourCategoryGate {
@@ -1,11 +1,62 @@
1
1
  import type { CatalogDocumentList, CatalogPageListResult, CatalogProductReferenceReadResult, CatalogueChannelCollectionsResponse, CatalogueCollectionsOptions, CatalogueDocumentAccessResult, CataloguePageRepresentationReadResult, CatalogueReferenceBatchResponse } from "./catalogue-types.js";
2
2
  import type { HttpTransport } from "./http-transport.js";
3
3
  import type { VariantId } from "./identifiers.js";
4
+ import type { IdentifySelectedPageInput, IdentifySelectedPageResult } from "./identify-types.js";
4
5
  import type { SamplizeCatalogueOptions, SamplizeCataloguePage } from "./samplize-catalogue-types.js";
6
+ import type { SamplizeColorVecCache } from "./samplize-colorvec.js";
5
7
  /** Catalogue publication operations inherited by the public SDK client. */
6
8
  export declare class CatalogueClient {
7
9
  #private;
8
10
  protected constructor(transport: HttpTransport);
11
+ /** Identify the canonical graph subject for one explicitly selected page. */
12
+ identify(input: IdentifySelectedPageInput, signal?: AbortSignal): Promise<IdentifySelectedPageResult>;
13
+ /** Complete producer-owned vectors and classifications for portable colour matching. */
14
+ samplizeColorVecCache(sourceVersion: string, signal?: AbortSignal): Promise<SamplizeColorVecCache>;
15
+ /** MG-owned full-catalogue facets and directional colour refinement before page limiting. */
16
+ samplizeCatalogueSearch(input: {
17
+ palette?: Array<{
18
+ hex: string;
19
+ proportion: number;
20
+ }>;
21
+ query?: string;
22
+ reference?: string;
23
+ direction?: "similar" | "lighter" | "darker" | "muted";
24
+ lightnessMin?: number;
25
+ lightnessMax?: number;
26
+ temperatureMin?: number;
27
+ temperatureMax?: number;
28
+ family?: string[];
29
+ lightness?: string[];
30
+ intensity?: string[];
31
+ brand?: string[];
32
+ sourceVersion: string;
33
+ limit?: number;
34
+ }, signal?: AbortSignal): Promise<{
35
+ target?: {
36
+ ref: string;
37
+ hex: string;
38
+ name?: string;
39
+ source: string;
40
+ confidence?: number;
41
+ };
42
+ embeddingVersion?: string;
43
+ rankingVersion?: string;
44
+ dictionaryVersion?: string;
45
+ eligibleCount?: number;
46
+ groups?: Array<{
47
+ hex: string;
48
+ proportion: number;
49
+ quota: number;
50
+ ids: string[];
51
+ shortfall: number;
52
+ }>;
53
+ ids: string[];
54
+ limited: boolean;
55
+ sourceVersion: string;
56
+ counts: Record<string, Record<string, number>>;
57
+ countBasis: string;
58
+ basis: string;
59
+ }>;
9
60
  /** Complete version-pinned Samplize paint corpus; stock never limits eligibility. */
10
61
  samplizeCatalogue(options?: SamplizeCatalogueOptions, signal?: AbortSignal): Promise<SamplizeCataloguePage>;
11
62
  /**
@@ -5,6 +5,18 @@ export class CatalogueClient {
5
5
  constructor(transport) {
6
6
  this.#transport = transport;
7
7
  }
8
+ /** Identify the canonical graph subject for one explicitly selected page. */
9
+ identify(input, signal) {
10
+ return this.#transport.requestJson(resolveOperation("retrieval.identify"), { body: input, signal });
11
+ }
12
+ /** Complete producer-owned vectors and classifications for portable colour matching. */
13
+ samplizeColorVecCache(sourceVersion, signal) {
14
+ return this.#transport.requestJson(resolveOperation("samplize_colorvec_cache"), { query: { sourceVersion }, signal });
15
+ }
16
+ /** MG-owned full-catalogue facets and directional colour refinement before page limiting. */
17
+ samplizeCatalogueSearch(input, signal) {
18
+ return this.#transport.requestJson(resolveOperation("samplize_catalogue_search"), { body: input, signal });
19
+ }
8
20
  /** Complete version-pinned Samplize paint corpus; stock never limits eligibility. */
9
21
  samplizeCatalogue(options = {}, signal) {
10
22
  return this.#transport.requestJson(resolveOperation("samplize_catalogue_products"), { query: { ...options }, signal });
@@ -1,3 +1,6 @@
1
+ import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsParams, ColourStatsResponse } from "./colour-aggregate-types.js";
2
+ import type { QueryParams } from "./http-transport.js";
3
+ import type { SdkOperationId } from "./operations.js";
1
4
  /**
2
5
  * The colour surface, namespaced as `mg.colours` — the aggregate reads
3
6
  * (`stats`, `profiles`) plus MULTI-COLOUR search (`palette`, `gradient`).
@@ -32,9 +35,7 @@
32
35
  * FROM PRODUCT IMAGERY, not declared by the manufacturer. Report visual
33
36
  * likeness, never a stated colour specification.
34
37
  */
35
- import type { CategoryColourProfilesResponse, ColourProfilesParams, ColourStatsParams, ColourStatsResponse } from "./colour-aggregate-types.js";
36
- import type { QueryParams } from "./http-transport.js";
37
- import type { SdkOperationId } from "./operations.js";
38
+ import type { PaletteCompleteInput, PaletteCompleteResponse, PaletteDiscoverInput, PaletteDiscoverResponse } from "./palette-completion-types.js";
38
39
  import type { SearchGradientInput, SearchGradientResponse, SearchPaletteInput, SearchPaletteResponse } from "./palette-search-types.js";
39
40
  /** The client's own request primitives, handed down so this file owns no transport. */
40
41
  export interface ColoursTransport {
@@ -44,6 +45,10 @@ export interface ColoursTransport {
44
45
  export declare class ColoursClient {
45
46
  #private;
46
47
  constructor(transport: ColoursTransport);
48
+ /** Complete exact retained colors against a shared published interior reference. No Cart mutation. */
49
+ complete(input: PaletteCompleteInput, signal?: AbortSignal): Promise<PaletteCompleteResponse>;
50
+ /** Browse diverse published interior compositions against a version-pinned eligible catalogue. */
51
+ discover(input: PaletteDiscoverInput, signal?: AbortSignal): Promise<PaletteDiscoverResponse>;
47
52
  /**
48
53
  * THE QUESTION: "what colours does this CATEGORY come in?" — the representative
49
54
  * palette of 1–8 categories in ONE call (GET `/api/v1/colours/profiles`).
package/colours-client.js CHANGED
@@ -3,6 +3,14 @@ export class ColoursClient {
3
3
  constructor(transport) {
4
4
  this.#transport = transport;
5
5
  }
6
+ /** Complete exact retained colors against a shared published interior reference. No Cart mutation. */
7
+ complete(input, signal) {
8
+ return this.#transport.post("complete_interior_palette", input, signal);
9
+ }
10
+ /** Browse diverse published interior compositions against a version-pinned eligible catalogue. */
11
+ discover(input, signal) {
12
+ return this.#transport.get("discover_interior_palettes", { ...input }, signal);
13
+ }
6
14
  /**
7
15
  * THE QUESTION: "what colours does this CATEGORY come in?" — the representative
8
16
  * palette of 1–8 categories in ONE call (GET `/api/v1/colours/profiles`).
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Public wire types for `POST /api/v1/identify`.
3
+ *
4
+ * These intentionally mirror the domain-owned Zod contract rather than
5
+ * importing it: the published SDK must carry no workspace package dependency.
6
+ */
7
+ export interface IdentifySelectedPageInput {
8
+ url: string;
9
+ title?: string;
10
+ selectedText?: string;
11
+ productLabels?: string[];
12
+ brandLabels?: string[];
13
+ /** Manufacturer code visibly supplied by the selected page. */
14
+ manufacturerCode?: string;
15
+ imageUrls?: string[];
16
+ colourHexes?: string[];
17
+ }
18
+ export interface IdentifySelectedPageSubject {
19
+ kind: "variant" | "product" | "brand";
20
+ id: string;
21
+ name: string;
22
+ brand?: {
23
+ id: string;
24
+ name: string;
25
+ slug: string;
26
+ };
27
+ }
28
+ export interface IdentifySelectedPageEvidence {
29
+ kind: "manufacturer-url" | "brand-homepage" | "visible-identity" | "visual" | "colour";
30
+ url?: string;
31
+ detail: string;
32
+ }
33
+ interface IdentifySelectedPageDataBase {
34
+ receipt: {
35
+ submittedUrl: string;
36
+ normalizedUrl: string;
37
+ normalizationSteps: string[];
38
+ };
39
+ evidence: IdentifySelectedPageEvidence[];
40
+ }
41
+ export type IdentifySelectedPageData = (IdentifySelectedPageDataBase & {
42
+ outcome: "identified";
43
+ rung: "lookup" | "disambiguate";
44
+ subject: IdentifySelectedPageSubject & {
45
+ kind: "variant";
46
+ };
47
+ }) | (IdentifySelectedPageDataBase & {
48
+ outcome: "placed";
49
+ rung: "lookup";
50
+ subject: IdentifySelectedPageSubject & {
51
+ kind: "product" | "brand";
52
+ };
53
+ }) | (IdentifySelectedPageDataBase & {
54
+ outcome: "ambiguous";
55
+ rung: "lookup" | "disambiguate" | "match" | "color";
56
+ candidates: IdentifySelectedPageSubject[];
57
+ truncated: boolean;
58
+ }) | (IdentifySelectedPageDataBase & {
59
+ outcome: "unresolved";
60
+ rung?: "match" | "color";
61
+ });
62
+ /** The canonical callable-operation envelope returned by `identify()`. */
63
+ export interface IdentifySelectedPageResult {
64
+ data: IdentifySelectedPageData;
65
+ meta: {
66
+ operationId: "retrieval.identify";
67
+ requestId: string;
68
+ };
69
+ links: Record<string, never>;
70
+ warnings: [];
71
+ }
72
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public wire types for `POST /api/v1/identify`.
3
+ *
4
+ * These intentionally mirror the domain-owned Zod contract rather than
5
+ * importing it: the published SDK must carry no workspace package dependency.
6
+ */
7
+ export {};
package/index.d.ts CHANGED
@@ -19,7 +19,9 @@ export { MATERIALGRAPH_PROBLEM_EXTENSIONS, MaterialGraphError, } from "./errors.
19
19
  export type { MaterialGraphErrorInit, MaterialGraphErrorKind, MaterialGraphRequestMetadata, ProblemDetail, RateLimit, SanitizedErrorValue, SanitizedIssue, SimpleErrorBody, } from "./errors.js";
20
20
  export type * from "./types.js";
21
21
  export type * from "./search-collapse-types.js";
22
+ export type * from "./search-card-types.js";
22
23
  export type * from "./match-types.js";
24
+ export type * from "./identify-types.js";
23
25
  export type * from "./browse-types.js";
24
26
  export type * from "./catalogue-types.js";
25
27
  export type * from "./by-colour-types.js";
@@ -45,7 +47,9 @@ export type * from "./schedule-reference-types.js";
45
47
  export type * from "./substitute-types.js";
46
48
  export type * from "./search-refinement-types.js";
47
49
  export type * from "./search-window-types.js";
50
+ export type * from "./attribution-types.js";
48
51
  export { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID } from "./spec-types.js";
49
52
  export type * from "./spec-types.js";
50
53
  export { certificationProgramme, certificationProgrammeCriterion, hasSpec, specCriterion, specRangeCriterion, specTerm, } from "./specifications.js";
51
54
  export type { SamplizePublicationSource, SamplizeLrvObservation, SamplizeDemand, SamplizeCollectionMembership, SamplizeCatalogueOptions, SamplizeCataloguePage, SamplizeCatalogueProduct, SamplizeHexObservation, } from "./samplize-catalogue-types.js";
55
+ export type { PaletteCompleteInput, PaletteCompleteResponse, PaletteDiscoverInput, PaletteDiscoverResponse, } from "./palette-completion-types.js";
@@ -1,3 +1,4 @@
1
+ import type { ResponseAttribution } from "./attribution-types.js";
1
2
  import type { MatchCategoryInference, MatchColourReading } from "./match-types.js";
2
3
  /**
3
4
  * The Material Match PROGRESS-STREAM contracts — the wire vocabulary of
@@ -179,6 +180,18 @@ export type MatchSenseCheckVerdict = (typeof MATCH_SENSE_CHECK_VERDICTS)[number]
179
180
  export declare const MATCH_OUTCOMES: readonly ["matched", "no_surfaces"];
180
181
  export type MatchOutcome = (typeof MATCH_OUTCOMES)[number];
181
182
  export interface MatchWorkflowResult {
183
+ /**
184
+ * Result attribution (MG-1131) — `responseId` (the same value as the run's
185
+ * `X-Request-Id` header), `basis: "match"`, and `issuedAt`. Every item in
186
+ * `results` and in each `surfaces[].results` carries a matching `resultId`.
187
+ *
188
+ * A surface's hits arrive twice — on its own `surface` frame and again here —
189
+ * and both times with the SAME `resultId`, so a consumer that stored the
190
+ * first recognises the second rather than double-counting it.
191
+ *
192
+ * ABSENT only from a response produced by a server predating MG-1131.
193
+ */
194
+ attribution?: ResponseAttribution;
182
195
  imageType: MatchImageType;
183
196
  /** How the run ended. Absent means `matched`. */
184
197
  outcome?: MatchOutcome;
package/match-types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ResponseAttribution } from "./attribution-types.js";
1
2
  import type { CorpusScope } from "./corpus-scope-types.js";
2
3
  /**
3
4
  * @module sdk/match-types
@@ -37,6 +38,15 @@ export interface MatchColourReading {
37
38
  measurable: boolean;
38
39
  }
39
40
  export interface MatchResponse {
41
+ /**
42
+ * Result attribution (MG-1131). `basis` is always `"match"` here — this route
43
+ * has no `mode`, so this is the field that lets a Match outcome be grouped
44
+ * alongside a `search()` one. Store it beside the query and each item's
45
+ * `resultId` beside the outcome.
46
+ *
47
+ * ABSENT only from a response produced by a server predating MG-1131.
48
+ */
49
+ attribution?: ResponseAttribution;
40
50
  /** THIS answer, capped by `limit`. Not a total. */
41
51
  results: SearchResultItem[];
42
52
  inferredCategories: MatchCategoryInference[];
@@ -52,6 +52,10 @@ export interface MaterialBlockProjectionField {
52
52
  value: MaterialBlockFieldValue;
53
53
  unit?: string;
54
54
  status: MaterialBlockFieldStatus;
55
+ /** Provenance supplied by the graph when a source-backed claim supports this field. */
56
+ sourceBasis?: string;
57
+ /** Claim confidence supplied by the graph; absence means no confidence was emitted. */
58
+ confidence?: number;
55
59
  }
56
60
  export interface MaterialBlockProjectionItem {
57
61
  canonicalBlockId: string;
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 ["scheduleReferences.list", "GET", "/api/v1/schedules/{scheduleId}/references", "list_schedule_reference_feedback"], readonly ["scheduleReferences.put", "PUT", "/api/v1/schedules/{scheduleId}/references/{referenceId}", "put_schedule_reference_feedback"], readonly ["catalogs", "GET", "/api/v1/catalogs", "list_catalog_documents"], readonly ["catalogPages", "GET", "/api/v1/catalogs/{catalogId}/pages", "list_catalog_pages"], readonly ["catalogPageRepresentation", "GET", "/api/v1/catalog-pages/{pageId}/representation", "get_catalog_page_representation"], readonly ["catalogReferences", "GET", "/api/v1/catalogs/{catalogId}/references", "list_catalog_product_references"], readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["planQuery", "POST", "/api/v1/search/plan", "plan_material_query"], readonly ["samplizeCatalogue", "GET", "/api/v1/catalogues/samplize/products", "samplize_catalogue_products"], 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 ["catalogueReferences", "POST", "/api/v1/catalogues/references", "get_catalogue_references_batch"], readonly ["catalogueCollections", "GET", "/api/v1/catalogues/collections", "list_catalogue_channel_collections"], readonly ["catalogueDocumentAccess", "GET", "/api/v1/catalogues/documents/{documentId}", "get_catalogue_document_access"], 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 ["materialRenderAssets", "GET", "/api/v1/materials/{variantId}/render-assets", "get_material_render_assets"], readonly ["materialDocuments", "GET", "/api/v1/materials/{variantId}/documents", "list_material_documents"], readonly ["exploration.operators", "GET", "/api/v1/materials/{variantId}/operators", "list_material_operators"], readonly ["exploration.candidates", "POST", "/api/v1/materials/{variantId}/candidates", "read_material_candidates"], 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 ["findSubstitutes", "POST", "/api/v1/materials/{variantId}/substitutes", "find_material_substitutes"], readonly ["associatedFinishes", "GET", "/api/v1/associated/finishes", "list_associated_finishes"], readonly ["associatedProducts", "GET", "/api/v1/associated/products", "list_associated_products"], readonly ["applicability", "GET", "/api/v1/applicability", "get_applicability"], 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 ["resolve", "POST", "/api/v1/resolve", "resolve_phrases"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["productTypeCoverage", "GET", "/api/v1/taxonomy/coverage", "get_product_type_status_coverage"], 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 ["brandColourDeck", "GET", "/api/v1/brands/{brandSlug}/colours", "brand_colour_deck"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["sourceMaterialComments", "GET", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "list_source_material_comments"], readonly ["sourceMaterialComments", "POST", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "post_source_material_comment"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["listRuns", "GET", "/api/v1/runs", "list_public_runs"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["citation", "GET", "/api/v1/citations/{citationId}", "get_verdict_citation"], 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 ["slotPools", "POST", "/api/v1/schemes/pools", "slot_pools"], 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 ["colours.complete", "POST", "/api/v1/colours/palettes/complete", "complete_interior_palette"], readonly ["colours.discover", "GET", "/api/v1/colours/palettes", "discover_interior_palettes"], readonly ["samplizeColorVecCache", "GET", "/api/v1/catalogues/samplize/colorvec", "samplize_colorvec_cache"], readonly ["samplizeCatalogueSearch", "POST", "/api/v1/catalogues/samplize/search", "samplize_catalogue_search"], readonly ["scheduleReferences.list", "GET", "/api/v1/schedules/{scheduleId}/references", "list_schedule_reference_feedback"], readonly ["scheduleReferences.put", "PUT", "/api/v1/schedules/{scheduleId}/references/{referenceId}", "put_schedule_reference_feedback"], readonly ["catalogs", "GET", "/api/v1/catalogs", "list_catalog_documents"], readonly ["catalogPages", "GET", "/api/v1/catalogs/{catalogId}/pages", "list_catalog_pages"], readonly ["catalogPageRepresentation", "GET", "/api/v1/catalog-pages/{pageId}/representation", "get_catalog_page_representation"], readonly ["catalogReferences", "GET", "/api/v1/catalogs/{catalogId}/references", "list_catalog_product_references"], readonly ["search", "POST", "/api/v1/search", "search_materials_post"], readonly ["identify", "POST", "/api/v1/identify", "retrieval.identify"], readonly ["planQuery", "POST", "/api/v1/search/plan", "plan_material_query"], readonly ["samplizeCatalogue", "GET", "/api/v1/catalogues/samplize/products", "samplize_catalogue_products"], 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 ["catalogueReferences", "POST", "/api/v1/catalogues/references", "get_catalogue_references_batch"], readonly ["catalogueCollections", "GET", "/api/v1/catalogues/collections", "list_catalogue_channel_collections"], readonly ["catalogueDocumentAccess", "GET", "/api/v1/catalogues/documents/{documentId}", "get_catalogue_document_access"], 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 ["materialRenderAssets", "GET", "/api/v1/materials/{variantId}/render-assets", "get_material_render_assets"], readonly ["materialDocuments", "GET", "/api/v1/materials/{variantId}/documents", "list_material_documents"], readonly ["exploration.operators", "GET", "/api/v1/materials/{variantId}/operators", "list_material_operators"], readonly ["exploration.candidates", "POST", "/api/v1/materials/{variantId}/candidates", "read_material_candidates"], 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 ["findSubstitutes", "POST", "/api/v1/materials/{variantId}/substitutes", "find_material_substitutes"], readonly ["associatedFinishes", "GET", "/api/v1/associated/finishes", "list_associated_finishes"], readonly ["associatedProducts", "GET", "/api/v1/associated/products", "list_associated_products"], readonly ["applicability", "GET", "/api/v1/applicability", "get_applicability"], 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 ["resolve", "POST", "/api/v1/resolve", "resolve_phrases"], readonly ["bySpec", "GET", "/api/v1/search/specifications", "catalog_by_spec"], readonly ["taxonomy", "GET", "/api/v1/taxonomy/categories", "get_category_taxonomy"], readonly ["productTypeCoverage", "GET", "/api/v1/taxonomy/coverage", "get_product_type_status_coverage"], 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 ["brandColourDeck", "GET", "/api/v1/brands/{brandSlug}/colours", "brand_colour_deck"], readonly ["uploadSourceMaterial", "POST", "/api/v1/source-material", "upload_source_material"], readonly ["sourceMaterialBatch", "GET", "/api/v1/source-material/{batchId}", "get_source_material_batch"], readonly ["sourceMaterialComments.list", "GET", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "list_source_material_comments"], readonly ["sourceMaterialComments.post", "POST", "/api/v1/source-material/{batchId}/sources/{sourceId}/comments", "post_source_material_comment"], readonly ["capabilities", "GET", "/api/v1/capabilities", "list_public_capabilities"], readonly ["createRun", "POST", "/api/v1/runs", "create_public_run"], readonly ["listRuns", "GET", "/api/v1/runs", "list_public_runs"], readonly ["run", "GET", "/api/v1/runs/{runId}", "get_public_run"], readonly ["runSchedule", "GET", "/api/v1/runs/{runId}/schedule", "get_run_finish_schedule"], readonly ["citation", "GET", "/api/v1/citations/{citationId}", "get_verdict_citation"], 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 ["slotPools", "POST", "/api/v1/schemes/pools", "slot_pools"], 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
@@ -1,6 +1,30 @@
1
1
  import { MaterialGraphError } from "./errors.js";
2
2
  export const MATERIALGRAPH_API_VERSION = "0.7.0";
3
3
  const OPERATION_DEFINITIONS = [
4
+ [
5
+ "colours.complete",
6
+ "POST",
7
+ "/api/v1/colours/palettes/complete",
8
+ "complete_interior_palette",
9
+ ],
10
+ [
11
+ "colours.discover",
12
+ "GET",
13
+ "/api/v1/colours/palettes",
14
+ "discover_interior_palettes",
15
+ ],
16
+ [
17
+ "samplizeColorVecCache",
18
+ "GET",
19
+ "/api/v1/catalogues/samplize/colorvec",
20
+ "samplize_colorvec_cache",
21
+ ],
22
+ [
23
+ "samplizeCatalogueSearch",
24
+ "POST",
25
+ "/api/v1/catalogues/samplize/search",
26
+ "samplize_catalogue_search",
27
+ ],
4
28
  [
5
29
  "scheduleReferences.list",
6
30
  "GET",
@@ -33,6 +57,7 @@ const OPERATION_DEFINITIONS = [
33
57
  "list_catalog_product_references",
34
58
  ],
35
59
  ["search", "POST", "/api/v1/search", "search_materials_post"],
60
+ ["identify", "POST", "/api/v1/identify", "retrieval.identify"],
36
61
  ["planQuery", "POST", "/api/v1/search/plan", "plan_material_query"],
37
62
  [
38
63
  "samplizeCatalogue",
@@ -270,13 +295,13 @@ const OPERATION_DEFINITIONS = [
270
295
  "get_source_material_batch",
271
296
  ],
272
297
  [
273
- "sourceMaterialComments",
298
+ "sourceMaterialComments.list",
274
299
  "GET",
275
300
  "/api/v1/source-material/{batchId}/sources/{sourceId}/comments",
276
301
  "list_source_material_comments",
277
302
  ],
278
303
  [
279
- "sourceMaterialComments",
304
+ "sourceMaterialComments.post",
280
305
  "POST",
281
306
  "/api/v1/source-material/{batchId}/sources/{sourceId}/comments",
282
307
  "post_source_material_comment",
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./index.d.ts",
8
8
  "import": "./index.js"
9
+ },
10
+ "./samplize-colorvec": {
11
+ "types": "./samplize-colorvec.d.ts",
12
+ "import": "./samplize-colorvec.js"
9
13
  }
10
14
  },
15
+ "dependencies": {
16
+ "@instruments/colorscope": "7.6.0"
17
+ },
11
18
  "license": "Proprietary",
12
19
  "engines": {
13
20
  "node": ">=24.0.0"
@@ -0,0 +1,115 @@
1
+ /** Public wire types; the SDK does not load the producer corpus. */
2
+ export type CompositionalRole = "Foundation" | "Supporting" | "Accent";
3
+ export interface RetainedColor {
4
+ readonly id: string;
5
+ readonly hex: string;
6
+ readonly role?: CompositionalRole;
7
+ readonly paintVariantId?: string;
8
+ }
9
+ export interface CompletionRequest {
10
+ readonly inputs: readonly RetainedColor[];
11
+ readonly maxDistance?: number;
12
+ readonly limit?: number;
13
+ readonly white?: "published" | "perceptual";
14
+ readonly memberCount?: number;
15
+ readonly excludeHexes?: readonly string[];
16
+ }
17
+ export interface CompletionAssignment {
18
+ readonly input: RetainedColor;
19
+ readonly referenceIndex: number;
20
+ readonly referenceHex: string;
21
+ readonly distance: number;
22
+ }
23
+ export interface CompletionMember {
24
+ readonly hex: string;
25
+ readonly referenceHex: string;
26
+ readonly referenceIndex: number;
27
+ readonly inputIds: readonly string[];
28
+ readonly role: CompositionalRole;
29
+ readonly proportion: number;
30
+ readonly sourceRole: string | null;
31
+ readonly sourceRelation: string | null;
32
+ }
33
+ export interface PaletteCompletion {
34
+ readonly uses?: readonly {
35
+ readonly inputId: string;
36
+ readonly role: CompositionalRole;
37
+ readonly proportions: readonly number[];
38
+ }[];
39
+ readonly id: string;
40
+ readonly reference: {
41
+ readonly anchorIndex?: number | null;
42
+ readonly libraryId: string;
43
+ readonly paletteId: string;
44
+ readonly name: string | null;
45
+ readonly maker: string | null;
46
+ readonly intent: "joint" | "alternatives";
47
+ readonly provenance: "authored" | "derived" | "generated";
48
+ };
49
+ readonly assignments: readonly CompletionAssignment[];
50
+ readonly members: readonly CompletionMember[];
51
+ readonly evidenceBasis: "published-composition" | "published-pair" | "nearby-adaptation";
52
+ readonly proportionBasis: "sourced" | "proposed";
53
+ readonly worstDistance: number;
54
+ readonly totalDistance: number;
55
+ }
56
+ export type PaletteCompleteInput = CompletionRequest & {
57
+ sourceVersion: string;
58
+ };
59
+ export interface PaletteDiscoverInput {
60
+ sourceVersion: string;
61
+ character?: "warm" | "cool" | "very-light" | "light" | "dark" | "muted" | "soft" | "vivid";
62
+ moreLike?: string;
63
+ cursor?: string;
64
+ limit?: number;
65
+ }
66
+ export interface MaterializedPalette {
67
+ completion: PaletteCompletion;
68
+ samples: Array<{
69
+ retainedPaint?: {
70
+ variantId: string;
71
+ name: string;
72
+ maker: string;
73
+ };
74
+ referenceIndex: number;
75
+ inputIds: readonly string[];
76
+ hex: string;
77
+ product: {
78
+ variantId: string;
79
+ productId: string;
80
+ hex: string;
81
+ name: string;
82
+ brand: {
83
+ slug: string;
84
+ name: string;
85
+ };
86
+ } | null;
87
+ status: string;
88
+ distance: number | null;
89
+ units: "oklab-euclidean";
90
+ }>;
91
+ }
92
+ interface PaletteVersions {
93
+ sourceVersion: string;
94
+ corpusVersion: string;
95
+ algorithmVersion: string;
96
+ transport: "colorscope-package";
97
+ }
98
+ export interface PaletteCompleteResponse extends PaletteVersions {
99
+ results: MaterializedPalette[];
100
+ coverage: {
101
+ status: string;
102
+ supported: number;
103
+ };
104
+ tolerance: {
105
+ bound: number;
106
+ units: string;
107
+ policy: string;
108
+ };
109
+ }
110
+ export interface PaletteDiscoverResponse extends PaletteVersions {
111
+ results: MaterializedPalette[];
112
+ total: number;
113
+ nextCursor: string | null;
114
+ }
115
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ export declare const SAMPLIZE_COLORVEC_RANKING_VERSION = "solid-colorvec-cosine-v2";
2
+ export declare const SAMPLIZE_COLORVEC_EMBEDDING_VERSION = "oklch-anchor-quantiles-v12";
3
+ export interface SamplizeColorVecPaint {
4
+ variantId: string;
5
+ hex: string;
6
+ brand: string;
7
+ oklab: {
8
+ L: number;
9
+ a: number;
10
+ b: number;
11
+ };
12
+ family: string;
13
+ lightness: string;
14
+ intensity: string;
15
+ vector: number[] | string;
16
+ }
17
+ export interface SamplizeColorVecCache {
18
+ sourceVersion: string;
19
+ dictionaryVersion: string;
20
+ embeddingVersion: string;
21
+ rankingVersion: string;
22
+ generatedAt: string;
23
+ paints: SamplizeColorVecPaint[];
24
+ }
25
+ export interface SamplizeColorVecInput {
26
+ hex?: string;
27
+ reference?: string;
28
+ palette?: Array<{
29
+ hex: string;
30
+ proportion: number;
31
+ }>;
32
+ direction?: "similar" | "lighter" | "darker" | "muted";
33
+ brand?: string[];
34
+ family?: string[];
35
+ lightness?: string[];
36
+ intensity?: string[];
37
+ lightnessMin?: number;
38
+ lightnessMax?: number;
39
+ temperatureMin?: number;
40
+ temperatureMax?: number;
41
+ limit?: number;
42
+ }
43
+ export declare const samplizeSolidColorVector: (hex: string) => number[];
44
+ /** Canonical constraints are shared by full-cache similarity and constrained text retrieval. */
45
+ export declare function selectSamplizeEligiblePaints<T extends Omit<SamplizeColorVecPaint, "vector">>(paints: T[], input: SamplizeColorVecInput): {
46
+ reference: T | undefined;
47
+ eligible: T[];
48
+ counts: Record<"brand" | "family" | "intensity" | "lightness", Record<string, number>>;
49
+ };
50
+ /** Official portable MG ranking over its complete versioned sampling-channel projection. */
51
+ export declare function createSamplizeColorVecIndex(cache: SamplizeColorVecCache): {
52
+ search(input: SamplizeColorVecInput): {
53
+ ids: string[];
54
+ groups: {
55
+ hex: string;
56
+ proportion: number;
57
+ quota: number;
58
+ ids: string[];
59
+ shortfall: number;
60
+ }[];
61
+ limited: boolean;
62
+ eligibleCount: number;
63
+ counts: Record<"brand" | "family" | "intensity" | "lightness", Record<string, number>>;
64
+ sourceVersion: string;
65
+ dictionaryVersion: string;
66
+ embeddingVersion: string;
67
+ rankingVersion: string;
68
+ countBasis: "complete-catalogue";
69
+ };
70
+ };
@@ -0,0 +1,167 @@
1
+ import { EMBEDDING_VERSION, EMBEDDING_VECTOR_SIZE, embeddingCosineSimilarity, paletteToEmbedding, } from "@instruments/colorscope/embedding";
2
+ export const SAMPLIZE_COLORVEC_RANKING_VERSION = "solid-colorvec-cosine-v2";
3
+ export const SAMPLIZE_COLORVEC_EMBEDDING_VERSION = EMBEDDING_VERSION;
4
+ const decodeVector = (encoded) => {
5
+ const bytes = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
6
+ if (bytes.length !== EMBEDDING_VECTOR_SIZE * 4)
7
+ throw new Error("Invalid colour vector size.");
8
+ const view = new DataView(bytes.buffer);
9
+ return Array.from({ length: EMBEDDING_VECTOR_SIZE }, (_, i) => view.getFloat32(i * 4, true));
10
+ };
11
+ const facets = ["brand", "family", "lightness", "intensity"];
12
+ const identityOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
13
+ export const samplizeSolidColorVector = (hex) => {
14
+ if (!/^#[0-9a-f]{6}$/i.test(hex))
15
+ throw new Error("A six-digit published colour is required.");
16
+ return paletteToEmbedding([{ hex, proportion: 1 }], { validateOklab: true });
17
+ };
18
+ /** Canonical constraints are shared by full-cache similarity and constrained text retrieval. */
19
+ export function selectSamplizeEligiblePaints(paints, input) {
20
+ const reference = input.reference
21
+ ? paints.find((p) => p.variantId === input.reference)
22
+ : undefined;
23
+ if (input.reference && !reference)
24
+ throw new Error("The reference is absent from this catalogue version.");
25
+ const low = input.lightnessMin ?? 0, high = input.lightnessMax ?? 1;
26
+ const cool = input.temperatureMin ?? -0.4, warm = input.temperatureMax ?? 0.4;
27
+ if (![low, high, cool, warm].every(Number.isFinite) ||
28
+ low > high ||
29
+ cool > warm)
30
+ throw new Error("Appearance ranges must be finite and ordered.");
31
+ const candidates = paints.filter((p) => {
32
+ if (!reference)
33
+ return true;
34
+ if (p.variantId === reference.variantId)
35
+ return false;
36
+ if (input.direction === "lighter")
37
+ return p.oklab.L > reference.oklab.L;
38
+ if (input.direction === "darker")
39
+ return p.oklab.L < reference.oklab.L;
40
+ if (input.direction === "muted")
41
+ return (Math.hypot(p.oklab.a, p.oklab.b) <
42
+ Math.hypot(reference.oklab.a, reference.oklab.b));
43
+ return true;
44
+ });
45
+ const matches = (p, omit) => p.oklab.L >= low &&
46
+ p.oklab.L <= high &&
47
+ p.oklab.b >= cool &&
48
+ p.oklab.b <= warm &&
49
+ facets.every((f) => f === omit || !input[f]?.length || input[f].includes(p[f]));
50
+ const counts = {
51
+ brand: {},
52
+ family: {},
53
+ lightness: {},
54
+ intensity: {},
55
+ };
56
+ for (const p of candidates)
57
+ for (const f of facets)
58
+ if (matches(p, f))
59
+ counts[f][p[f]] = (counts[f][p[f]] ?? 0) + 1;
60
+ const eligible = candidates.filter((p) => matches(p));
61
+ return { reference, eligible, counts };
62
+ }
63
+ /** Official portable MG ranking over its complete versioned sampling-channel projection. */
64
+ export function createSamplizeColorVecIndex(cache) {
65
+ if (cache.embeddingVersion !== EMBEDDING_VERSION ||
66
+ cache.rankingVersion !== SAMPLIZE_COLORVEC_RANKING_VERSION)
67
+ throw new Error("The colour cache uses an incompatible embedding or ranking version. Refresh it before searching.");
68
+ if (!cache.sourceVersion ||
69
+ !cache.dictionaryVersion ||
70
+ !Number.isFinite(Date.parse(cache.generatedAt)))
71
+ throw new Error("The colour cache is missing its provenance.");
72
+ const paints = cache.paints.map((p) => ({
73
+ ...p,
74
+ vector: typeof p.vector === "string" ? decodeVector(p.vector) : p.vector,
75
+ }));
76
+ const identities = new Set();
77
+ for (const paint of paints) {
78
+ if (identities.has(paint.variantId) ||
79
+ paint.vector.length !== EMBEDDING_VECTOR_SIZE ||
80
+ !paint.vector.every(Number.isFinite) ||
81
+ ![paint.oklab.L, paint.oklab.a, paint.oklab.b].every(Number.isFinite))
82
+ throw new Error("The colour cache contains invalid or duplicate paint records.");
83
+ identities.add(paint.variantId);
84
+ }
85
+ const vectors = new Map();
86
+ const vectorFor = (hex) => {
87
+ const key = hex.toLowerCase();
88
+ let value = vectors.get(key);
89
+ if (!value) {
90
+ value = samplizeSolidColorVector(hex);
91
+ if (vectors.size >= 512)
92
+ vectors.delete(vectors.keys().next().value);
93
+ vectors.set(key, value);
94
+ }
95
+ return value;
96
+ };
97
+ return {
98
+ search(input) {
99
+ const limit = input.limit ?? 48;
100
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
101
+ throw new Error("Choose between 1 and 100 suggestions.");
102
+ const { reference, eligible, counts } = selectSamplizeEligiblePaints(paints, input);
103
+ const palette = (input.palette?.length
104
+ ? input.palette
105
+ : [{ hex: input.hex ?? reference?.hex ?? "", proportion: 1 }])
106
+ .map((p) => ({ ...p, hex: p.hex.toLowerCase() }))
107
+ .sort((a, b) => b.proportion - a.proportion || identityOrder(a.hex, b.hex));
108
+ if (palette.length > 12 ||
109
+ palette.some((p) => !Number.isFinite(p.proportion) ||
110
+ p.proportion <= 0 ||
111
+ p.proportion > 1))
112
+ throw new Error("Supply up to twelve positive image colour shares.");
113
+ const total = palette.reduce((sum, p) => sum + p.proportion, 0);
114
+ const buckets = palette.map((p, position) => ({
115
+ ...p,
116
+ position,
117
+ vector: vectorFor(p.hex),
118
+ quota: Math.floor((limit * p.proportion) / total),
119
+ remainder: ((limit * p.proportion) / total) % 1,
120
+ paints: [],
121
+ }));
122
+ let remaining = limit - buckets.reduce((sum, p) => sum + p.quota, 0);
123
+ for (const p of [...buckets].sort((a, b) => b.remainder - a.remainder || a.position - b.position))
124
+ if (remaining-- > 0)
125
+ p.quota++;
126
+ for (const paint of eligible) {
127
+ let best = buckets[0], score = embeddingCosineSimilarity(paint.vector, best.vector);
128
+ for (let i = 1; i < buckets.length; i++) {
129
+ const candidate = buckets[i], next = embeddingCosineSimilarity(paint.vector, candidate.vector);
130
+ if (next > score) {
131
+ best = candidate;
132
+ score = next;
133
+ }
134
+ }
135
+ best.paints.push({ id: paint.variantId, score });
136
+ }
137
+ const groups = buckets
138
+ .sort((a, b) => b.proportion - a.proportion || a.position - b.position)
139
+ .map((p) => {
140
+ const ids = p.paints
141
+ .sort((a, b) => b.score - a.score || identityOrder(a.id, b.id))
142
+ .slice(0, p.quota)
143
+ .map((paint) => paint.id);
144
+ return {
145
+ hex: p.hex,
146
+ proportion: p.proportion,
147
+ quota: p.quota,
148
+ ids,
149
+ shortfall: p.quota - ids.length,
150
+ };
151
+ });
152
+ const ids = groups.flatMap((p) => p.ids);
153
+ return {
154
+ ids,
155
+ groups,
156
+ limited: eligible.length > ids.length,
157
+ eligibleCount: eligible.length,
158
+ counts,
159
+ sourceVersion: cache.sourceVersion,
160
+ dictionaryVersion: cache.dictionaryVersion,
161
+ embeddingVersion: cache.embeddingVersion,
162
+ rankingVersion: cache.rankingVersion,
163
+ countBasis: "complete-catalogue",
164
+ };
165
+ },
166
+ };
167
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @module sdk/search-card-types
3
+ * @description The `view:"card"` search projection — the lean grid item and the
4
+ * envelope that carries it.
5
+ *
6
+ * Split out of `types.ts` — which is over the repository's source-size limit —
7
+ * along the same seam `match-types.ts` took: one view of one surface, and
8
+ * nothing else. The full-view item and the shared envelope stay in `types.ts`,
9
+ * which this module reads `SearchResponse` from.
10
+ */
11
+ import type { ProductId, VariantId } from "./identifiers.js";
12
+ import type { OpenEnum, SearchResponse } from "./types.js";
13
+ /**
14
+ * The lean `view:"card"` result item — identity + name + thumbnail + score.
15
+ *
16
+ * A PROJECTION, not a smaller answer: a field the card omits (palette, verdict,
17
+ * signals, availability) is unread here, never known-absent. Re-read the same
18
+ * `variantId` at `view: "full"` or via `material()` before claiming anything the
19
+ * card does not carry.
20
+ */
21
+ export interface SearchCardItem {
22
+ /** Accepted sampleability fact. Absent means unknown, never false. */
23
+ sampleable?: boolean;
24
+ /** Opaque — round-trip verbatim. */
25
+ variantId: VariantId;
26
+ productId: ProductId | null;
27
+ brandSlug: string | null;
28
+ brand: string;
29
+ /** Product name (the full item's `product`). */
30
+ name: string;
31
+ variantName: string | null;
32
+ imageUrl: string | null;
33
+ projectSaveCapability?: "allowed";
34
+ sourceStatus?: "provisional";
35
+ score: number;
36
+ /**
37
+ * Resolved values for the attribute ids sent in `returnAttributes`, keyed by
38
+ * attribute id. ABSENT entirely when `returnAttributes` was not sent.
39
+ *
40
+ * Three states, all different, and reading them as two is the mistake this
41
+ * shape exists to prevent: no `attributes` key means you did not ask; a key
42
+ * whose value is `null` means this material carries no accepted value for that
43
+ * attribute; anything else is what the graph holds. Values resolve with the
44
+ * normal variant-over-product precedence, and an array is a multi-valued
45
+ * attribute rather than a list of candidates.
46
+ */
47
+ attributes?: Record<string, string | string[] | null>;
48
+ }
49
+ /** The `view:"card"` response form — the lean grid projection of {@link SearchResponse}. */
50
+ export interface SearchCardResponse extends Omit<SearchResponse, "view" | "results"> {
51
+ view: OpenEnum<"card">;
52
+ results: SearchCardItem[];
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -57,6 +57,33 @@ export interface AvailableSubstitutesResponse {
57
57
  items: SubstituteResultItem[];
58
58
  candidatePoolSize: number;
59
59
  droppedUnavailable: number;
60
+ /**
61
+ * MG-1686, MG-1783. True when the candidate read hit the bounded pool ceiling,
62
+ * so more materials match the signature than were ranked. A cut pool is an
63
+ * even SAMPLE across the id keyspace rather than its lowest window, so the
64
+ * ranked set is representative of the match set without being all of it —
65
+ * treat the items as the best of a sample, never as the best that exist. A cut
66
+ * pool that produced NO items answers `inconclusive` instead, so `available`
67
+ * with an empty `items` always means a complete search that found nothing.
68
+ */
69
+ poolTruncated: boolean;
70
+ supersededBy: MaterialSuccessor | null;
71
+ nextCursor?: string;
72
+ }
73
+ /**
74
+ * MG-1686. The search read a bounded window of the matches and nothing in it
75
+ * reached the page, while more matches existed past the window. It is not an
76
+ * answer of "nothing can replace this" — that answer is `available` with an
77
+ * empty `items`. `reason` names what happened and the next move; follow
78
+ * `nextCursor` when it is present.
79
+ */
80
+ export interface InconclusiveSubstitutesResponse {
81
+ status: "inconclusive";
82
+ signatureUsed: AvailableSubstituteSignature;
83
+ items: [];
84
+ candidatePoolSize: number;
85
+ droppedUnavailable: number;
86
+ reason: string;
60
87
  supersededBy: MaterialSuccessor | null;
61
88
  nextCursor?: string;
62
89
  }
@@ -72,4 +99,4 @@ export interface InsufficientSubstitutesResponse {
72
99
  items: [];
73
100
  supersededBy: MaterialSuccessor | null;
74
101
  }
75
- export type FindSubstitutesResponse = AvailableSubstitutesResponse | UnsupportedSubstitutesResponse | InsufficientSubstitutesResponse;
102
+ export type FindSubstitutesResponse = AvailableSubstitutesResponse | InconclusiveSubstitutesResponse | UnsupportedSubstitutesResponse | InsufficientSubstitutesResponse;
package/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ResponseAttribution } from "./attribution-types.js";
1
2
  import type { BySpecVerdict } from "./by-spec-types.js";
2
3
  /**
3
4
  * Hand-mirrored wire types for the MaterialGraph public API (spec 0.5.0).
@@ -31,6 +32,7 @@ import type { Cursor, ProductId, VariantId } from "./identifiers.js";
31
32
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
32
33
  import type { QueryUnderstanding } from "./query-understanding-types.js";
33
34
  import type { CollapsedVariant, SearchAnatomyMember } from "./search-collapse-types.js";
35
+ import type { MoodInterpretation } from "./search-mood-types.js";
34
36
  import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
35
37
  import type { CompoundColourConstraintReceipt, CompoundFormConstraintReceipt, SearchExecutionPlan, SearchMode, SearchRefinementReceipt, SearchRerankReport, SearchResponseExplainReceipts } from "./search-response-receipt-types.js";
36
38
  import type { SearchWindowReceipt } from "./search-window-types.js";
@@ -114,6 +116,8 @@ export interface VisualMeaning {
114
116
  * `visualMeanings` is not "this material has no visual meaning".
115
117
  */
116
118
  export interface SearchResultItem {
119
+ /** This hit's attribution key — see {@link ResponseAttribution}. Opaque. */
120
+ resultId?: string;
117
121
  /** Accepted sampleability fact. Absent means unknown, never false. */
118
122
  sampleable?: boolean;
119
123
  /** Opaque — round-trip verbatim into `material()`, `pairings()`, `similarTo`. */
@@ -263,42 +267,6 @@ export interface SearchResultItem {
263
267
  */
264
268
  attributes?: Record<string, string | string[] | null>;
265
269
  }
266
- /**
267
- * The lean `view:"card"` result item — identity + name + thumbnail + score.
268
- *
269
- * A PROJECTION, not a smaller answer: a field the card omits (palette, verdict,
270
- * signals, availability) is unread here, never known-absent. Re-read the same
271
- * `variantId` at `view: "full"` or via `material()` before claiming anything the
272
- * card does not carry.
273
- */
274
- export interface SearchCardItem {
275
- /** Accepted sampleability fact. Absent means unknown, never false. */
276
- sampleable?: boolean;
277
- /** Opaque — round-trip verbatim. */
278
- variantId: VariantId;
279
- productId: ProductId | null;
280
- brandSlug: string | null;
281
- brand: string;
282
- /** Product name (the full item's `product`). */
283
- name: string;
284
- variantName: string | null;
285
- imageUrl: string | null;
286
- projectSaveCapability?: "allowed";
287
- sourceStatus?: "provisional";
288
- score: number;
289
- /**
290
- * Resolved values for the attribute ids sent in `returnAttributes`, keyed by
291
- * attribute id. ABSENT entirely when `returnAttributes` was not sent.
292
- *
293
- * Three states, all different, and reading them as two is the mistake this
294
- * shape exists to prevent: no `attributes` key means you did not ask; a key
295
- * whose value is `null` means this material carries no accepted value for that
296
- * attribute; anything else is what the graph holds. Values resolve with the
297
- * normal variant-over-product precedence, and an array is a multi-valued
298
- * attribute rather than a list of candidates.
299
- */
300
- attributes?: Record<string, string | string[] | null>;
301
- }
302
270
  /**
303
271
  * A feed total with its honesty basis — THE CLAIM ABOUT THE MATCH SET, which is
304
272
  * a different number from `results.length` (this page).
@@ -391,11 +359,29 @@ export interface SearchFacets {
391
359
  * wrong whenever `limit` was 24.
392
360
  */
393
361
  export interface SearchResponse extends SearchResponseExplainReceipts {
362
+ /** Result attribution — see {@link ResponseAttribution}. `basis` is `mode` verbatim here. */
363
+ attribution?: ResponseAttribution;
394
364
  view: OpenEnum<"full" | "card">;
395
365
  /** THIS PAGE. Its length is a page size, never a total. */
396
366
  results: SearchResultItem[];
397
367
  suggestions: SearchSuggestion[];
398
368
  mode: SearchMode;
369
+ /**
370
+ * Which dense text lane answered, when one did (MG-1796). `image-text` — the
371
+ * query carried evidence it describes appearance, so it was ranked against
372
+ * image vectors; `variant-text` — ranked against each variant's written
373
+ * portrait.
374
+ *
375
+ * ABSENT means the answer did not come from the dense text arm at all: `mode`
376
+ * was never `text`, or the text arm resolved without a lane. Absence is
377
+ * UNKNOWN and never a default of `image-text` — treating it as one asserts a
378
+ * dense reading of a lexical, colour or browse answer. Absent too from any
379
+ * response produced by a server predating MG-1793.
380
+ *
381
+ * An {@link OpenEnum} because the lane vocabulary can grow: a value you do
382
+ * not recognise is a lane this client predates, not a malformed response.
383
+ */
384
+ textLane?: OpenEnum<"image-text" | "variant-text">;
399
385
  maxScore: number;
400
386
  cursor?: Cursor;
401
387
  window?: SearchWindowReceipt;
@@ -432,7 +418,7 @@ export interface SearchResponse extends SearchResponseExplainReceipts {
432
418
  * Explicit standard-search mood reading; null ids mean no selected mood.
433
419
  * Absent when mood was not evaluated or an older server produced the result.
434
420
  */
435
- moodInterpretation?: import("./search-mood-types.js").MoodInterpretation;
421
+ moodInterpretation?: MoodInterpretation;
436
422
  /**
437
423
  * Present when the query had multiple defensible readings. Says which one
438
424
  * ran, or leaves every reading non-default when the engine chose none. Every
@@ -453,11 +439,6 @@ export interface SearchResponse extends SearchResponseExplainReceipts {
453
439
  /** A `getVariantBrief` echo of the seed variant, present only in `variantId` seed mode. */
454
440
  seed?: MaterialTile | Record<string, unknown>;
455
441
  }
456
- /** The `view:"card"` response form — the lean grid projection of {@link SearchResponse}. */
457
- export interface SearchCardResponse extends Omit<SearchResponse, "view" | "results"> {
458
- view: OpenEnum<"card">;
459
- results: SearchCardItem[];
460
- }
461
442
  export type ColourDictionaryId = OpenEnum<"colour_family" | "colour_mood" | "colour_undertone">;
462
443
  export interface ColourFacetTerm {
463
444
  id: string;
@@ -606,6 +587,13 @@ export interface PairingsResponse {
606
587
  * check it before presenting the rail as demand evidence.
607
588
  */
608
589
  cospecCount: number;
590
+ /**
591
+ * Candidates dropped at hydration for violating the material contract
592
+ * (MG-985). Present ONLY when > 0. A non-zero count means the rail is
593
+ * shorter than the evidence behind it — read it before calling the rail
594
+ * complete.
595
+ */
596
+ skippedCandidates?: number;
609
597
  /** Present only when `order` was requested. Evidence tiers remain independent. */
610
598
  ordering?: PairingOrdering;
611
599
  /** Opaque continuation bound to the seed, filters, corpus, and order. */