@instruments/materialgraph 3.0.2 → 3.2.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.
Files changed (57) hide show
  1. package/association-types.d.ts +20 -20
  2. package/brand-types.d.ts +6 -6
  3. package/by-spec-types.d.ts +6 -6
  4. package/catalog-discovery-types.d.ts +4 -4
  5. package/categories-client.d.ts +3 -3
  6. package/category-types.d.ts +18 -18
  7. package/client-config.d.ts +2 -2
  8. package/client-response-schema.d.ts +2 -2
  9. package/client-response-schema.js +4 -4
  10. package/client-vocabulary.js +1 -1
  11. package/client.d.ts +1 -1
  12. package/client.js +14 -10
  13. package/color-profiles-types.d.ts +8 -8
  14. package/color-wire-types.d.ts +6 -6
  15. package/colors-client.d.ts +3 -3
  16. package/discovery-context-types.d.ts +8 -8
  17. package/errors.d.ts +12 -13
  18. package/errors.js +84 -74
  19. package/find-sampled-together-types.d.ts +6 -6
  20. package/find-variants-by-color-types.d.ts +30 -30
  21. package/find-variants-by-vector-types.d.ts +13 -13
  22. package/get-variant-types.d.ts +11 -7
  23. package/http-transport.d.ts +14 -10
  24. package/http-transport.js +14 -12
  25. package/image-reference-types.d.ts +2 -2
  26. package/index.d.ts +2 -2
  27. package/json.d.ts +18 -0
  28. package/json.js +36 -0
  29. package/list-enum-values-types.d.ts +8 -8
  30. package/list-sampled-together-pairs-types.d.ts +6 -6
  31. package/list-variant-facets-types.d.ts +8 -8
  32. package/list-variants-types.d.ts +59 -29
  33. package/material-asset-types.d.ts +37 -24
  34. package/material-block-types.d.ts +6 -6
  35. package/material-types.d.ts +6 -6
  36. package/nearest-paints-types.d.ts +8 -8
  37. package/operation-responses.d.ts +2 -2
  38. package/operations.d.ts +1 -1
  39. package/operations.js +1 -1
  40. package/package.json +1 -1
  41. package/parse-operation-response.d.ts +4 -3
  42. package/parse-operation-response.js +112 -135
  43. package/resolve-color-types.d.ts +6 -6
  44. package/resolve-variant-types.d.ts +10 -10
  45. package/response-schema-catalog.d.ts +8 -0
  46. package/response-schema-catalog.js +82 -0
  47. package/response-schemas.d.ts +32 -3
  48. package/response-schemas.generated.js +26 -25
  49. package/response-schemas.js +5 -77
  50. package/schema-resource-types.d.ts +12 -12
  51. package/search-catalog-subset-types.d.ts +2 -2
  52. package/search-variants-types.d.ts +12 -12
  53. package/source-material-types.d.ts +6 -6
  54. package/source-material.js +2 -32
  55. package/types.d.ts +38 -38
  56. package/webmcp.d.ts +10 -4
  57. package/webmcp.generated.js +5 -4
@@ -20,6 +20,13 @@ export type VariantAssetAvailability = OpenEnum<"available" | "unknown" | "unava
20
20
  export type VariantImageRole = OpenEnum<"product" | "swatch" | "detail" | "scene" | "technical" | "color_chart" | "sample_collection" | "other" | "unknown">;
21
21
  /** How an image must be framed, a different question from what it shows. */
22
22
  export type VariantImagePresentation = OpenEnum<"edge_to_edge" | "packshot" | "cutout_margin" | "montage" | "unknown">;
23
+ /** What a photo shows of its material, so an app can place it at real size. */
24
+ export type VariantImageShows = OpenEnum<"sample" | "crop" | "laid_field" | "room" | "detail">;
25
+ /** The size of the surface in an image's frame, in millimeters. */
26
+ export interface VariantImageSurfaceMm {
27
+ width_mm: number;
28
+ height_mm: number;
29
+ }
23
30
  /** What a document is for; a document can carry more than one purpose. */
24
31
  export type VariantDocumentType = OpenEnum<"technical_data_sheet" | "product_sheet" | "technical_bulletin" | "sustainability_report" | "specification" | "installation_guide" | "care_maintenance_guide" | "operation_manual" | "warranty" | "parts_list" | "technical_drawing" | "test_report" | "certificate" | "declaration_of_performance" | "environmental_product_declaration" | "health_product_declaration" | "ingredient_disclosure" | "safety_data_sheet" | "brochure" | "catalog" | "color_finish_chart" | "other" | "unknown">;
25
32
  /** Every asset kind the registry knows. */
@@ -29,7 +36,7 @@ export type VariantAssetKind = OpenEnum<"image" | "document" | "cad" | "bim" | "
29
36
  * appear on their kind, and an absent value is unknown. `url` is absent when
30
37
  * `availability` is `unavailable`; `appliesTo` appears only as `product`.
31
38
  */
32
- export type VariantAssetItem = {
39
+ export interface VariantAssetItem {
33
40
  /** Only `product`: filed against the whole product. Absent: this variant's own. */
34
41
  appliesTo?: "product";
35
42
  assetId: string;
@@ -43,23 +50,29 @@ export type VariantAssetItem = {
43
50
  /** Images only; absent when unknown. */
44
51
  imageRole?: VariantImageRole;
45
52
  kind: VariantAssetKind;
53
+ /** Millimeters per pixel of this file. Images only; absent when unknown. */
54
+ mmPerPx?: number;
46
55
  /** Not on images; absent when unknown. */
47
56
  name?: string;
48
57
  /** Images only; absent when unknown. */
49
58
  presentation?: VariantImagePresentation;
59
+ /** What the photo shows of the material. Images only; absent when unknown. */
60
+ shows?: VariantImageShows;
61
+ /** The surface in frame: width and height times `mmPerPx`. Images only; absent when unknown. */
62
+ surfaceMm?: VariantImageSurfaceMm;
50
63
  url?: string;
51
64
  /** Images only; absent when unknown. */
52
65
  width?: number;
53
- };
66
+ }
54
67
  /** Whether the brand's document source was read for this variant. Absent when documents are filtered out. */
55
68
  export type VariantDocumentCoverageState = OpenEnum<"available" | "none_declared" | "not_ingested" | "unavailable_source" | "dead_link" | "failed_ingest">;
56
- export type VariantAssetPageInfo = {
69
+ export interface VariantAssetPageInfo {
57
70
  nextCursor: string | null;
58
71
  returnedCount: number;
59
72
  totalMatches: number;
60
- };
73
+ }
61
74
  /** Request filters and paging for `listVariantAssets`. Each list is OR within itself, AND across lists. */
62
- export type ListVariantAssetsOptions = {
75
+ export interface ListVariantAssetsOptions {
63
76
  kinds?: VariantAssetKind[];
64
77
  imageRoles?: VariantImageRole[];
65
78
  documentTypes?: VariantDocumentType[];
@@ -67,28 +80,28 @@ export type ListVariantAssetsOptions = {
67
80
  limit?: number;
68
81
  /** `pageInfo.nextCursor` from the previous page, with the same variantId and filters. */
69
82
  cursor?: string;
70
- };
83
+ }
71
84
  /** One page. An unknown variant is a 404, never a page. */
72
- export type VariantAssetPage = {
85
+ export interface VariantAssetPage {
73
86
  documentCoverage?: VariantDocumentCoverageState;
74
87
  items: VariantAssetItem[];
75
88
  pageInfo: VariantAssetPageInfo;
76
89
  /** The variant's primary image. Absent when it has none. */
77
90
  primaryAssetId?: string;
78
- };
91
+ }
79
92
  export type MaterialDocumentStatus = OpenEnum<"downloaded" | "linked_not_downloaded" | "unavailable_source" | "dead_link" | "failed_ingest">;
80
93
  export type MaterialDocumentCoverageState = OpenEnum<"available" | "none_declared" | "not_ingested" | "unavailable_source" | "dead_link" | "failed_ingest">;
81
- export type MaterialDocumentScope = {
94
+ export interface MaterialDocumentScope {
82
95
  kind: "product" | "variant";
83
96
  id: string;
84
- };
85
- export type MaterialDocumentSource = {
97
+ }
98
+ export interface MaterialDocumentSource {
86
99
  /** Opaque, public identifier for one source collection; never a PIM/DAM path. */
87
100
  id: string;
88
101
  authority: string;
89
102
  revision: string;
90
- };
91
- export type MaterialDocumentProjectionItem = {
103
+ }
104
+ export interface MaterialDocumentProjectionItem {
92
105
  id: string;
93
106
  scope: MaterialDocumentScope & {
94
107
  inherited: boolean;
@@ -113,34 +126,34 @@ export type MaterialDocumentProjectionItem = {
113
126
  contentType: string | null;
114
127
  fileExtension: string | null;
115
128
  source: MaterialDocumentSource;
116
- };
117
- export type MaterialDocumentCoverageSource = {
129
+ }
130
+ export interface MaterialDocumentCoverageSource {
118
131
  scope: MaterialDocumentScope;
119
132
  state: MaterialDocumentCoverageState;
120
133
  exact: boolean;
121
134
  declaredCount: number | null;
122
135
  readableCount: number;
123
136
  source: MaterialDocumentSource;
124
- };
125
- export type MaterialDocumentCoverage = {
137
+ }
138
+ export interface MaterialDocumentCoverage {
126
139
  productUnion: MaterialDocumentCoverageSummary;
127
140
  requestedVariant: MaterialDocumentCoverageSummary;
128
141
  sources: MaterialDocumentCoverageSource[];
129
- };
130
- export type MaterialDocumentCoverageSummary = {
142
+ }
143
+ export interface MaterialDocumentCoverageSummary {
131
144
  state: MaterialDocumentCoverageState;
132
145
  exact: boolean;
133
146
  declaredCount: number | null;
134
147
  readableCount: number;
135
- };
136
- export type MaterialDocumentPageOptions = {
148
+ }
149
+ export interface MaterialDocumentPageOptions {
137
150
  cursor?: string;
138
151
  limit?: number;
139
- };
140
- export type MaterialDocumentPage = {
152
+ }
153
+ export interface MaterialDocumentPage {
141
154
  variantId: VariantId;
142
155
  documents: MaterialDocumentProjectionItem[];
143
156
  coverage: MaterialDocumentCoverage;
144
157
  totalDocuments: number;
145
158
  nextCursor: string | null;
146
- };
159
+ }
@@ -51,7 +51,7 @@ export type MaterialBlockFieldValue = string | number | boolean | unknown[] | Re
51
51
  * `status: "complete"` is a real negative you may publish; the same `false`
52
52
  * under `missing` is a placeholder for an unanswered question.
53
53
  */
54
- export type MaterialBlockProjectionField = {
54
+ export interface MaterialBlockProjectionField {
55
55
  /** Canonical vocabulary, not an opaque id - the same token `listVariants` filters accept. */
56
56
  attributeId: string;
57
57
  label: string;
@@ -64,21 +64,21 @@ export type MaterialBlockProjectionField = {
64
64
  valueSource?: string;
65
65
  /** Claim confidence supplied by the graph; absence means no confidence was emitted. */
66
66
  confidence?: number;
67
- };
68
- export type MaterialBlockProjectionItem = {
67
+ }
68
+ export interface MaterialBlockProjectionItem {
69
69
  canonicalBlockId: string;
70
70
  canonicalBlockName: string;
71
71
  status: MaterialBlockStatus;
72
72
  fields: MaterialBlockProjectionField[];
73
73
  /** The work queue: attributes with `status: "missing"`. UNKNOWN, never "none". */
74
74
  missingAttributeIds: string[];
75
- };
75
+ }
76
76
  /**
77
77
  * Every canonical block for one material, each field carrying whether it is
78
78
  * determined. The whole surface, not a page — there is no cursor here.
79
79
  */
80
- export type MaterialBlockProjection = {
80
+ export interface MaterialBlockProjection {
81
81
  /** Opaque — echoed from the request. */
82
82
  variantId: VariantId;
83
83
  blocks: MaterialBlockProjectionItem[];
84
- };
84
+ }
@@ -34,7 +34,7 @@ export type MaterialAvailability = OpenEnum<"available" | "discontinued" | "supe
34
34
  * The grid-card projection of a material. Tiles carry `availability` but not a
35
35
  * successor record.
36
36
  */
37
- export type MaterialTile = {
37
+ export interface MaterialTile {
38
38
  /** Accepted sampleability fact. Absent means unknown, never false. */
39
39
  sampleable?: boolean;
40
40
  /** Opaque — round-trip verbatim into every other material read. */
@@ -57,9 +57,9 @@ export type MaterialTile = {
57
57
  * the attribute is absent — never render a missing key as "unnamed".
58
58
  */
59
59
  colorName?: string;
60
- };
60
+ }
61
61
  /** A retained product-type proposal. It is review evidence, never a category. */
62
- type ProposedProductType = {
62
+ interface ProposedProductType {
63
63
  id: string;
64
64
  displayName: string;
65
65
  confidence: number | null;
@@ -67,10 +67,10 @@ type ProposedProductType = {
67
67
  sourceUrl: string | null;
68
68
  isPrimary: boolean;
69
69
  scope: "variant" | "product";
70
- };
70
+ }
71
71
  /** Accepted display categories remain separate from this review-state receipt. */
72
- export type ProductTypeReceipt = {
72
+ export interface ProductTypeReceipt {
73
73
  state: "accepted" | "proposed" | "unknown";
74
74
  proposedProductTypes: ProposedProductType[];
75
- };
75
+ }
76
76
  export {};
@@ -1,10 +1,10 @@
1
1
  import type { SearchCatalogSubset } from "./search-catalog-subset-types.js";
2
- export type NearestPaintsParams = {
2
+ export interface NearestPaintsParams {
3
3
  hexes: string[];
4
4
  limit?: number;
5
5
  searchCatalogSubset?: SearchCatalogSubset;
6
- };
7
- export type NearestPaintCard = {
6
+ }
7
+ export interface NearestPaintCard {
8
8
  variantId: string;
9
9
  name: string;
10
10
  brand: {
@@ -25,11 +25,11 @@ export type NearestPaintCard = {
25
25
  lrv: number | null;
26
26
  deltaE: number;
27
27
  closeness: "very-near" | "very-close" | "close" | "related" | "unrelated";
28
- };
29
- export type NearestPaintsHexResult = {
28
+ }
29
+ export interface NearestPaintsHexResult {
30
30
  hex: string;
31
31
  paints: NearestPaintCard[];
32
- };
33
- export type NearestPaintsOutput = {
32
+ }
33
+ export interface NearestPaintsOutput {
34
34
  results: NearestPaintsHexResult[];
35
- };
35
+ }
@@ -20,7 +20,7 @@ import type { SchemaResourceResponse } from "./schema-resource-types.js";
20
20
  import type { SearchVariantsOutput } from "./search-variants-types.js";
21
21
  import type { SourceMaterialBatchReceipt } from "./source-material-types.js";
22
22
  import type { ExtractPaletteResponse, ListAttributesResult, PairingsResponse, ResolveColorResponse, WhoamiResponse } from "./types.js";
23
- export type OperationResponses = {
23
+ export interface OperationResponses {
24
24
  get_discovery_reference: DiscoveryReference;
25
25
  interpret_discovery_terms: DiscoveryTerms;
26
26
  find_colorless_variants: ColorlessVariantsResult;
@@ -56,5 +56,5 @@ export type OperationResponses = {
56
56
  search_variants: SearchVariantsOutput;
57
57
  upload_source_material: SourceMaterialBatchReceipt;
58
58
  whoami: WhoamiResponse;
59
- };
59
+ }
60
60
  export type JsonOperationId = keyof OperationResponses;
package/operations.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type SdkOperation, type SdkOperationId } from "./operations.generated.js";
1
+ import type { SdkOperation, SdkOperationId } from "./operations.generated.js";
2
2
  export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS, } from "./operations.generated.js";
3
3
  export type { SdkOperation, SdkOperationId } from "./operations.generated.js";
4
4
  /** Look up a registered operation by its OpenAPI `operationId`. */
package/operations.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MaterialGraphError } from "./errors.js";
2
- import { SDK_OPERATIONS, } from "./operations.generated.js";
2
+ import { SDK_OPERATIONS } from "./operations.generated.js";
3
3
  export { MATERIALGRAPH_API_VERSION, SDK_OPERATIONS, } from "./operations.generated.js";
4
4
  /** Look up a registered operation by its OpenAPI `operationId`. */
5
5
  export const findSdkOperation = (operationId) => SDK_OPERATIONS.find((candidate) => candidate.operationId === operationId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materialgraph",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,5 +1,6 @@
1
+ import type { JsonValue } from "./json-value-types.js";
2
+ import type { JsonObject } from "./json.js";
1
3
  import type { JsonOperationId, OperationResponses } from "./operation-responses.js";
2
- import type { JsonSchema } from "./response-schemas.js";
3
4
  /** Evaluate the JSON Schema keywords used by the published response contracts. */
4
- export declare const matchesResponseSchema: (value: unknown, schema: unknown, root: JsonSchema) => boolean;
5
- export declare const parseOperationResponse: <K extends JsonOperationId>(operationId: K, value: unknown) => Promise<OperationResponses[K]>;
5
+ export declare const matchesResponseSchema: (value: JsonValue | undefined, schema: JsonValue | undefined, root: JsonObject) => boolean;
6
+ export declare const parseOperationResponse: <K extends JsonOperationId>(operationId: K, value: JsonValue | undefined) => Promise<OperationResponses[K]>;
@@ -1,185 +1,162 @@
1
1
  import { openClientResponseEnums } from "./client-response-schema.js";
2
2
  import { MaterialGraphError } from "./errors.js";
3
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ import { isJsonNumber, isJsonObject, isJsonString, jsonKind } from "./json.js";
4
4
  const sameJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
5
+ const SIMPLE_TYPES = new Set([
6
+ "array",
7
+ "boolean",
8
+ "null",
9
+ "object",
10
+ "string",
11
+ ]);
5
12
  const matchesType = (value, type) => {
6
13
  if (Array.isArray(type)) {
7
14
  return type.some((candidate) => matchesType(value, candidate));
8
15
  }
9
- switch (type) {
10
- case undefined: {
11
- return true;
12
- }
13
- case "null": {
14
- return value === null;
15
- }
16
- case "array": {
17
- return Array.isArray(value);
18
- }
19
- case "object": {
20
- return isRecord(value);
21
- }
22
- case "integer": {
23
- return typeof value === "number" && Number.isInteger(value);
24
- }
25
- case "number": {
26
- return typeof value === "number" && Number.isFinite(value);
27
- }
28
- case "string": {
29
- return typeof value === "string";
30
- }
31
- case "boolean": {
32
- return typeof value === "boolean";
33
- }
34
- default: {
35
- return false;
36
- }
16
+ if (type === undefined) {
17
+ return true;
18
+ }
19
+ if (type === "integer") {
20
+ return isJsonNumber(value) && Number.isInteger(value);
21
+ }
22
+ if (type === "number") {
23
+ return isJsonNumber(value) && Number.isFinite(value);
37
24
  }
25
+ return (isJsonString(type) && SIMPLE_TYPES.has(type) && jsonKind(value) === type);
38
26
  };
39
27
  const matchesNumberKeywords = (value, schema) => {
40
- if (typeof value === "number") {
41
- if (typeof schema.minimum === "number" && value < schema.minimum) {
42
- return false;
28
+ if (!isJsonNumber(value)) {
29
+ return true;
30
+ }
31
+ const { exclusiveMaximum, exclusiveMinimum, maximum, minimum } = schema;
32
+ if (isJsonNumber(minimum) && value < minimum) {
33
+ return false;
34
+ }
35
+ if (isJsonNumber(maximum) && value > maximum) {
36
+ return false;
37
+ }
38
+ if (isJsonNumber(exclusiveMinimum) && value <= exclusiveMinimum) {
39
+ return false;
40
+ }
41
+ return !(isJsonNumber(exclusiveMaximum) && value >= exclusiveMaximum);
42
+ };
43
+ const matchesStringKeywords = (value, schema) => {
44
+ if (!isJsonString(value)) {
45
+ return true;
46
+ }
47
+ const { maxLength, minLength, pattern } = schema;
48
+ let length = 0;
49
+ for (const _codePoint of value) {
50
+ length += 1;
51
+ }
52
+ if (isJsonNumber(minLength) && length < minLength) {
53
+ return false;
54
+ }
55
+ if (isJsonNumber(maxLength) && length > maxLength) {
56
+ return false;
57
+ }
58
+ return !(isJsonString(pattern) && !new RegExp(pattern, "u").test(value));
59
+ };
60
+ const matchesValueKeywords = (value, schema) => {
61
+ if ("const" in schema && !sameJson(value, schema.const)) {
62
+ return false;
63
+ }
64
+ const { enum: allowed } = schema;
65
+ return !(Array.isArray(allowed) &&
66
+ !allowed.some((candidate) => sameJson(candidate, value)));
67
+ };
68
+ /** Evaluates the JSON Schema keywords the published response contracts use. */
69
+ class ResponseSchemaEvaluator {
70
+ #root;
71
+ constructor(root) {
72
+ this.#root = root;
73
+ }
74
+ matches(value, schema) {
75
+ if (schema === true || schema === false) {
76
+ return schema;
43
77
  }
44
- if (typeof schema.maximum === "number" && value > schema.maximum) {
78
+ if (!isJsonObject(schema)) {
45
79
  return false;
46
80
  }
47
- if (typeof schema.exclusiveMinimum === "number" &&
48
- value <= schema.exclusiveMinimum) {
81
+ if (!this.#matchesReference(value, schema)) {
49
82
  return false;
50
83
  }
51
- if (typeof schema.exclusiveMaximum === "number" &&
52
- value >= schema.exclusiveMaximum) {
84
+ if (!matchesType(value, schema.type)) {
53
85
  return false;
54
86
  }
55
- }
56
- return true;
57
- };
58
- const matchesStringKeywords = (value, schema) => {
59
- if (typeof value === "string") {
60
- let length = 0;
61
- for (const _codePoint of value) {
62
- length += 1;
63
- }
64
- if (typeof schema.minLength === "number" && length < schema.minLength) {
87
+ if (!matchesValueKeywords(value, schema)) {
65
88
  return false;
66
89
  }
67
- if (typeof schema.maxLength === "number" && length > schema.maxLength) {
90
+ if (Array.isArray(schema.anyOf) &&
91
+ !schema.anyOf.some((candidate) => this.matches(value, candidate))) {
68
92
  return false;
69
93
  }
70
- if (typeof schema.pattern === "string" &&
71
- !new RegExp(schema.pattern, "u").test(value)) {
94
+ if ("not" in schema && this.matches(value, schema.not)) {
72
95
  return false;
73
96
  }
97
+ return (matchesNumberKeywords(value, schema) &&
98
+ matchesStringKeywords(value, schema) &&
99
+ this.#matchesArrayKeywords(value, schema) &&
100
+ this.#matchesObjectKeywords(value, schema));
74
101
  }
75
- return true;
76
- };
77
- const matchesArrayKeywords = (value, schema, root) => {
78
- if (Array.isArray(value)) {
79
- if (typeof schema.minItems === "number" && value.length < schema.minItems) {
102
+ #matchesArrayKeywords(value, schema) {
103
+ if (!Array.isArray(value)) {
104
+ return true;
105
+ }
106
+ const { items, maxItems, minItems, prefixItems } = schema;
107
+ if (isJsonNumber(minItems) && value.length < minItems) {
80
108
  return false;
81
109
  }
82
- if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
110
+ if (isJsonNumber(maxItems) && value.length > maxItems) {
83
111
  return false;
84
112
  }
85
- const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : [];
113
+ const prefix = Array.isArray(prefixItems) ? prefixItems : [];
86
114
  for (const [index, item] of value.entries()) {
87
- const itemSchema = index < prefix.length ? prefix[index] : schema.items;
88
- if (itemSchema !== undefined &&
89
- !matchesResponseSchema(item, itemSchema, root)) {
115
+ const itemSchema = index < prefix.length ? prefix[index] : items;
116
+ if (itemSchema !== undefined && !this.matches(item, itemSchema)) {
90
117
  return false;
91
118
  }
92
119
  }
120
+ return true;
93
121
  }
94
- return true;
95
- };
96
- const matchesObjectKeywords = (value, schema, root) => {
97
- if (isRecord(value)) {
98
- if (Array.isArray(schema.required) &&
99
- !schema.required.every((key) => typeof key === "string" && Object.hasOwn(value, key))) {
122
+ #matchesObjectKeywords(value, schema) {
123
+ if (!isJsonObject(value)) {
124
+ return true;
125
+ }
126
+ const { additionalProperties, propertyNames, required } = schema;
127
+ if (Array.isArray(required) &&
128
+ !required.every((key) => isJsonString(key) && Object.hasOwn(value, key))) {
100
129
  return false;
101
130
  }
102
- const properties = isRecord(schema.properties) ? schema.properties : {};
131
+ const properties = isJsonObject(schema.properties) ? schema.properties : {};
103
132
  for (const [key, item] of Object.entries(value)) {
104
- if (schema.propertyNames !== undefined &&
105
- !matchesResponseSchema(key, schema.propertyNames, root)) {
133
+ if (propertyNames !== undefined && !this.matches(key, propertyNames)) {
106
134
  return false;
107
135
  }
108
136
  const property = Object.hasOwn(properties, key)
109
137
  ? properties[key]
110
- : schema.additionalProperties;
111
- if (property !== undefined &&
112
- !matchesResponseSchema(item, property, root)) {
138
+ : additionalProperties;
139
+ if (property !== undefined && !this.matches(item, property)) {
113
140
  return false;
114
141
  }
115
142
  }
143
+ return true;
116
144
  }
117
- return true;
118
- };
119
- const matchesResponseReference = (value, schema, root) => {
120
- if (typeof schema.$ref === "string") {
121
- if (!schema.$ref.startsWith("#/$defs/")) {
122
- return false;
145
+ #matchesReference(value, schema) {
146
+ const reference = schema.$ref;
147
+ if (!isJsonString(reference)) {
148
+ return true;
123
149
  }
124
- const name = schema.$ref
125
- .slice(8)
126
- .replaceAll("~1", "/")
127
- .replaceAll("~0", "~");
128
- if (!isRecord(root.$defs) ||
129
- !matchesResponseSchema(value, root.$defs[name], root)) {
150
+ if (!reference.startsWith("#/$defs/")) {
130
151
  return false;
131
152
  }
153
+ const name = reference.slice(8).replaceAll("~1", "/").replaceAll("~0", "~");
154
+ const definitions = this.#root.$defs;
155
+ return isJsonObject(definitions) && this.matches(value, definitions[name]);
132
156
  }
133
- return true;
134
- };
135
- const matchesResponseValue = (value, schema) => {
136
- if ("const" in schema && !sameJson(value, schema.const)) {
137
- return false;
138
- }
139
- if (Array.isArray(schema.enum) &&
140
- !schema.enum.some((candidate) => sameJson(candidate, value))) {
141
- return false;
142
- }
143
- return true;
144
- };
157
+ }
145
158
  /** Evaluate the JSON Schema keywords used by the published response contracts. */
146
- export const matchesResponseSchema = (value, schema, root) => {
147
- if (typeof schema === "boolean") {
148
- return schema;
149
- }
150
- if (!isRecord(schema)) {
151
- return false;
152
- }
153
- if (!matchesResponseReference(value, schema, root)) {
154
- return false;
155
- }
156
- if (!matchesType(value, schema.type)) {
157
- return false;
158
- }
159
- if (!matchesResponseValue(value, schema)) {
160
- return false;
161
- }
162
- if (Array.isArray(schema.anyOf) &&
163
- !schema.anyOf.some((candidate) => matchesResponseSchema(value, candidate, root))) {
164
- return false;
165
- }
166
- if ("not" in schema && matchesResponseSchema(value, schema.not, root)) {
167
- return false;
168
- }
169
- if (!matchesNumberKeywords(value, schema)) {
170
- return false;
171
- }
172
- if (!matchesStringKeywords(value, schema)) {
173
- return false;
174
- }
175
- if (!matchesArrayKeywords(value, schema, root)) {
176
- return false;
177
- }
178
- if (!matchesObjectKeywords(value, schema, root)) {
179
- return false;
180
- }
181
- return true;
182
- };
159
+ export const matchesResponseSchema = (value, schema, root) => new ResponseSchemaEvaluator(root).matches(value, schema);
183
160
  const schemas = new Map();
184
161
  /** The operation's published schema, rather than a caller's type argument, proves its response. */
185
162
  const isOperationResponse = (operationId, value) => {
@@ -190,8 +167,8 @@ export const parseOperationResponse = async (operationId, value) => {
190
167
  let schema = schemas.get(operationId);
191
168
  if (!schema) {
192
169
  // oxlint-disable-next-line howells/no-runtime-dynamic-imports -- Load the large response schema bundle only when a response needs validation.
193
- const { responseSchema } = await import("./response-schemas.js");
194
- schema = responseSchema(operationId);
170
+ const catalog = await import("./response-schema-catalog.js");
171
+ schema = catalog.buildResponseSchema(operationId);
195
172
  openClientResponseEnums(schema);
196
173
  schemas.set(operationId, schema);
197
174
  }
@@ -7,15 +7,15 @@ import type { VariantId } from "./identifiers.js";
7
7
  import type { SearchCatalogSubset } from "./search-catalog-subset-types.js";
8
8
  import type { OpenEnum } from "./types.js";
9
9
  /** Options for `resolveColor`. */
10
- export type ResolveColorOptions = {
10
+ export interface ResolveColorOptions {
11
11
  /**
12
12
  * Sets `inScope` on each catalog variant the lookup reports. It never
13
13
  * narrows the lookup. Absent leaves every `inScope` null.
14
14
  */
15
15
  searchCatalogSubset?: SearchCatalogSubset;
16
- };
16
+ }
17
17
  /** One catalog variant carrying the named paint. */
18
- export type ResolveColorCatalogVariant = {
18
+ export interface ResolveColorCatalogVariant {
19
19
  variantId: VariantId;
20
20
  /** The brand exactly as stored, suspect or not. */
21
21
  brand: {
@@ -25,7 +25,7 @@ export type ResolveColorCatalogVariant = {
25
25
  };
26
26
  /** Whether the variant is in the requested scope; null when none was asked for. */
27
27
  inScope: boolean | null;
28
- };
28
+ }
29
29
  /**
30
30
  * Whether the named paint is a variant in the catalog. Exact, global, and
31
31
  * never narrowed by `searchCatalogSubset`, and run only for a `brand` library hit.
@@ -43,7 +43,7 @@ export type ResolveColorVariant = ({
43
43
  status: "not-looked-up";
44
44
  reason: OpenEnum<"reference-color" | "descriptive-color" | "no-paint-identity" | "generic-color" | "several-paints">;
45
45
  };
46
- export type ResolveColorResponse = {
46
+ export interface ResolveColorResponse {
47
47
  /** Examples within the governed range. They do not replace its representative or bounds. */
48
48
  rangeSamples?: string[];
49
49
  /** Explicit brand choices for an ambiguous paint name. Never selected automatically. */
@@ -73,4 +73,4 @@ export type ResolveColorResponse = {
73
73
  /** The catalog lookup for the name. Absent from servers that predate it. */
74
74
  variant?: ResolveColorVariant;
75
75
  } | null;
76
- };
76
+ }