@instruments/materialgraph 0.26.0 → 0.27.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 (113) hide show
  1. package/association-types.d.ts +18 -7
  2. package/attribution-types.d.ts +2 -2
  3. package/brand-types.d.ts +8 -0
  4. package/brands-client.d.ts +5 -36
  5. package/brands-client.js +7 -33
  6. package/by-colour-types.d.ts +40 -13
  7. package/by-spec-types.d.ts +0 -67
  8. package/by-spec-types.js +7 -5
  9. package/catalog-discovery-types.d.ts +1 -16
  10. package/catalogue-client.d.ts +12 -31
  11. package/catalogue-client.js +10 -46
  12. package/catalogue-types.d.ts +40 -0
  13. package/categories-client.d.ts +63 -0
  14. package/categories-client.js +48 -0
  15. package/category-types.d.ts +114 -0
  16. package/client-config.d.ts +2 -4
  17. package/client-config.js +2 -4
  18. package/client-vocabulary.d.ts +2 -4
  19. package/client-vocabulary.js +11 -9
  20. package/client.d.ts +103 -203
  21. package/client.js +126 -251
  22. package/codes-types.d.ts +21 -0
  23. package/colours-client.d.ts +4 -9
  24. package/colours-client.js +0 -8
  25. package/errors.d.ts +1 -1
  26. package/errors.js +1 -0
  27. package/identify-types.d.ts +49 -39
  28. package/identify-types.js +5 -3
  29. package/index.d.ts +11 -21
  30. package/index.js +4 -4
  31. package/list-variant-facets-types.d.ts +60 -0
  32. package/list-variants-types.d.ts +142 -0
  33. package/list-variants-types.js +12 -0
  34. package/match-stream-types.d.ts +13 -0
  35. package/match-stream-types.js +2 -0
  36. package/match-stream.js +12 -8
  37. package/match-types.d.ts +24 -0
  38. package/material-asset-types.d.ts +69 -49
  39. package/material-block-types.d.ts +2 -0
  40. package/material-composition-client.d.ts +21 -0
  41. package/material-composition-client.js +13 -0
  42. package/material-composition-types.d.ts +403 -0
  43. package/material-query-plan-types.d.ts +57 -141
  44. package/material-resources-client.d.ts +9 -7
  45. package/material-resources-client.js +19 -4
  46. package/material-types.d.ts +6 -14
  47. package/operations.d.ts +1 -1
  48. package/operations.js +20 -117
  49. package/package.json +5 -1
  50. package/palette-search-types.d.ts +59 -24
  51. package/query-understanding-types.d.ts +27 -5
  52. package/resolve-client.d.ts +6 -10
  53. package/resolve-client.js +5 -7
  54. package/response-schemas.d.ts +20 -0
  55. package/response-schemas.generated.d.ts +5 -0
  56. package/response-schemas.generated.js +160 -0
  57. package/response-schemas.js +113 -0
  58. package/run-types.d.ts +12 -93
  59. package/runs.d.ts +1 -37
  60. package/runs.js +2 -79
  61. package/schema-resource-types.d.ts +14 -1
  62. package/search-card-types.d.ts +22 -0
  63. package/search-index-plan-types.d.ts +70 -0
  64. package/search-refinement-types.d.ts +8 -38
  65. package/search-refinement-types.js +1 -0
  66. package/search-request-types.d.ts +117 -200
  67. package/search-response-receipt-types.d.ts +12 -268
  68. package/search-result-validation.d.ts +0 -16
  69. package/search-result-validation.js +22 -180
  70. package/spec-facet-types.d.ts +3 -8
  71. package/spec-types.d.ts +17 -10
  72. package/spec-types.js +8 -7
  73. package/specifications.d.ts +13 -55
  74. package/specifications.js +29 -73
  75. package/suggest-types.d.ts +4 -10
  76. package/suggest-types.js +1 -1
  77. package/types.d.ts +135 -338
  78. package/webmcp.d.ts +1 -1
  79. package/webmcp.generated.js +10 -94
  80. package/brand-most-sampled-types.d.ts +0 -33
  81. package/browse-types.d.ts +0 -236
  82. package/client-browse.d.ts +0 -78
  83. package/client-browse.js +0 -71
  84. package/client-exploration.d.ts +0 -9
  85. package/client-exploration.js +0 -5
  86. package/compare-types.d.ts +0 -89
  87. package/composition-client.d.ts +0 -33
  88. package/composition-client.js +0 -28
  89. package/composition-types.d.ts +0 -455
  90. package/composition-types.js +0 -10
  91. package/correction-types.d.ts +0 -88
  92. package/exploration-types.d.ts +0 -118
  93. package/exploration-types.js +0 -1
  94. package/fan-deck-types.d.ts +0 -42
  95. package/fan-deck-types.js +0 -9
  96. package/match-rejection-types.d.ts +0 -43
  97. package/match-rejection-types.js +0 -7
  98. package/palette-completion-types.d.ts +0 -115
  99. package/palette-completion-types.js +0 -1
  100. package/render-assets-types.d.ts +0 -65
  101. package/render-assets-types.js +0 -1
  102. package/search-collapse-types.d.ts +0 -69
  103. package/search-collapse-types.js +0 -1
  104. package/search-mood-types.d.ts +0 -6
  105. package/search-mood-types.js +0 -1
  106. package/search-window-types.d.ts +0 -25
  107. package/search-window-types.js +0 -1
  108. package/slot-pool-types.d.ts +0 -372
  109. package/slot-pool-types.js +0 -1
  110. /package/{brand-most-sampled-types.js → category-types.js} +0 -0
  111. /package/{browse-types.js → list-variant-facets-types.js} +0 -0
  112. /package/{compare-types.js → material-composition-types.js} +0 -0
  113. /package/{correction-types.js → search-index-plan-types.js} +0 -0
@@ -1,20 +1,4 @@
1
1
  import type { ValidationFailure } from "./validation.js";
2
- /** `verdictStateSchema` — the six live cell states. */
3
- export declare const VERDICT_STATES: readonly ["matched", "deviated", "failed", "unverified", "missing", "not_evaluated"];
4
- /** `attributeVerdictSchema.strength`. */
5
- export declare const VERDICT_STRENGTHS: readonly ["required", "preferred"];
6
- /** `verdictBaselineSchema` discriminant — the four arms of what a cell was judged against. */
7
- export declare const VERDICT_BASELINE_KINDS: readonly ["term", "presence", "seed", "image"];
8
- /** The `image` baseline arm's own `source` enum. */
9
- export declare const VERDICT_IMAGE_SOURCES: readonly ["variant", "vector"];
10
- /** `verdictDeviationSchema.direction`. */
11
- export declare const VERDICT_DEVIATION_DIRECTIONS: readonly ["above", "below", "other"];
12
- /** `verdictTrustGapSchema` — why a real value did not clear the trust bar. */
13
- export declare const VERDICT_TRUST_GAPS: readonly ["below_confidence", "basis_unmet", "proposed"];
14
- /** `verdictGradeSchema` — the rolled-up card grade. */
15
- export declare const VERDICT_GRADES: readonly ["meets_all", "close", "hard_no"];
16
- /** `visualMeaningSchema.basis` — where a meaning came from. */
17
- export declare const VISUAL_MEANING_BASES: readonly ["canonical_attribute", "image_colour_analysis"];
18
2
  /**
19
3
  * Prove one wire value is a {@link SearchResultItem}, raising through `fail` at
20
4
  * the first field that does not match `searchResultItemSchema`. Checks the shape
@@ -1,44 +1,23 @@
1
- import { requireArray, requireBoolean, requireEnum, requireNonEmptyString, requireNonEmptyStringArray, requireNullableBoundedNumber, requireNullableEnum, requireNullableNonEmptyString, requireNullableNumber, requireNullableString, requireNumber, requireOptionalBoolean, requireOptionalNumber, requireOptionalString, requireRecord, requireString, requireIntegerAtLeast, } from "./validation.js";
2
- // ── Closed enum members (canonical `z.enum` mirrors) ─────────────────────────
3
- /** `verdictStateSchema` — the six live cell states. */
4
- export const VERDICT_STATES = [
5
- "matched",
6
- "deviated",
7
- "failed",
8
- "unverified",
9
- "missing",
10
- "not_evaluated",
11
- ];
12
- /** `attributeVerdictSchema.strength`. */
13
- export const VERDICT_STRENGTHS = ["required", "preferred"];
14
- /** `verdictBaselineSchema` discriminant — the four arms of what a cell was judged against. */
15
- export const VERDICT_BASELINE_KINDS = [
16
- "term",
17
- "presence",
18
- "seed",
19
- "image",
20
- ];
21
- /** The `image` baseline arm's own `source` enum. */
22
- export const VERDICT_IMAGE_SOURCES = ["variant", "vector"];
23
- /** `verdictDeviationSchema.direction`. */
24
- export const VERDICT_DEVIATION_DIRECTIONS = [
25
- "above",
26
- "below",
27
- "other",
28
- ];
29
- /** `verdictTrustGapSchema` — why a real value did not clear the trust bar. */
30
- export const VERDICT_TRUST_GAPS = [
31
- "below_confidence",
32
- "basis_unmet",
33
- "proposed",
34
- ];
35
- /** `verdictGradeSchema` — the rolled-up card grade. */
36
- export const VERDICT_GRADES = ["meets_all", "close", "hard_no"];
37
- /** `visualMeaningSchema.basis` — where a meaning came from. */
38
- export const VISUAL_MEANING_BASES = [
39
- "canonical_attribute",
40
- "image_colour_analysis",
41
- ];
1
+ /**
2
+ * The one runtime validator for a {@link SearchResultItem} on the wire — the
3
+ * hand-rolled, zero-runtime-dependency mirror of `searchResultItemSchema`
4
+ * (`packages/products/src/search/search-result-schema.ts`) and its full
5
+ * transitive closure, including `searchResultSignalsSchema`.
6
+ *
7
+ * WHY THIS EXISTS SEPARATELY FROM THE TYPE MIRROR. `contract-parity.test.ts`
8
+ * proves the SDK's STATIC types match the canonical schemas. That is a
9
+ * compile-time proof about shapes; it validates nothing at run time. A malformed
10
+ * result item arriving over a stream was caught by neither, so the Match reader
11
+ * now checks every item through here.
12
+ *
13
+ * ONE HOME. This module is the single implementation — no copy lives inside
14
+ * `match-stream.ts`. It is package-internal (not re-exported from `index.ts`)
15
+ * because a published symbol is a permanent contract, and nothing outside the
16
+ * SDK needs to call it yet; promoting it later is additive.
17
+ *
18
+ * ENUMS ARE CLOSED, matching the canonical `z.enum(…)` members.
19
+ */
20
+ import { requireArray, requireBoolean, requireEnum, requireNullableNumber, requireNullableString, requireNumber, requireOptionalBoolean, requireOptionalString, requireRecord, requireString, } from "./validation.js";
42
21
  // ── Leaf checks ──────────────────────────────────────────────────────────────
43
22
  /** `palette[]` — `{ hex, proportion, name: string.nullable().optional() }`. */
44
23
  const checkPaletteSwatch = (value, what, fail) => {
@@ -50,23 +29,6 @@ const checkPaletteSwatch = (value, what, fail) => {
50
29
  requireNullableString(record.name, `${what}.name`, fail);
51
30
  }
52
31
  };
53
- /**
54
- * `variants[]` — one entry of a product-collapsed result's ranked variant list
55
- * (`collapsedVariantSchema`). A SLIM projection, not a nested result item: the
56
- * checks here are the whole shape.
57
- */
58
- const checkCollapsedVariant = (value, what, fail) => {
59
- const record = requireRecord(value, what, fail);
60
- requireString(record.variantId, `${what}.variantId`, fail);
61
- requireNullableString(record.variantName, `${what}.variantName`, fail);
62
- requireNullableString(record.color, `${what}.color`, fail);
63
- // `.optional()`, NOT nullable: the emit path omits the key when no palette is
64
- // on record rather than sending null.
65
- requireOptionalString(record.hex, `${what}.hex`, fail);
66
- requireNullableString(record.imageUrl, `${what}.imageUrl`, fail);
67
- requireOptionalString(record.materialBankId, `${what}.materialBankId`, fail);
68
- requireNumber(record.score, `${what}.score`, fail);
69
- };
70
32
  /** `colourMeta` — three independently optional strings. */
71
33
  const checkColourMeta = (value, what, fail) => {
72
34
  const record = requireRecord(value, what, fail);
@@ -74,112 +36,10 @@ const checkColourMeta = (value, what, fail) => {
74
36
  requireOptionalString(record.dominantFamily, `${what}.dominantFamily`, fail);
75
37
  requireOptionalString(record.dominantHex, `${what}.dominantHex`, fail);
76
38
  };
77
- /** `searchResultSignalsSchema` — five required scores plus additive portrait. */
39
+ /** `searchResultSignalsSchema` — the Match image cosine, nullable. */
78
40
  const checkSignals = (value, what, fail) => {
79
41
  const record = requireRecord(value, what, fail);
80
- if (record.portrait !== undefined) {
81
- requireNullableNumber(record.portrait, `${what}.portrait`, fail);
82
- }
83
- for (const signal of ["image", "category", "colour", "lexical", "text"]) {
84
- requireNullableNumber(record[signal], `${what}.${signal}`, fail);
85
- }
86
- };
87
- /** `visualMeaningSchema` — note the NON-EMPTY `values` array and the closed `basis`. */
88
- const checkVisualMeaning = (value, what, fail) => {
89
- const record = requireRecord(value, what, fail);
90
- requireString(record.id, `${what}.id`, fail);
91
- requireString(record.label, `${what}.label`, fail);
92
- requireNonEmptyStringArray(record.values, `${what}.values`, fail);
93
- requireEnum(record.basis, VISUAL_MEANING_BASES, `${what}.basis`, fail);
94
- requireOptionalNumber(record.confidence, `${what}.confidence`, fail);
95
- requireOptionalString(record.sourceBasis, `${what}.sourceBasis`, fail);
96
- requireOptionalString(record.version, `${what}.version`, fail);
97
- requireOptionalString(record.appliedAt, `${what}.appliedAt`, fail);
98
- };
99
- // ── Verdict subtree ──────────────────────────────────────────────────────────
100
- /**
101
- * `verdictBaselineSchema` — a discriminated union on `kind`. An unknown
102
- * discriminant is rejected, exactly as `z.discriminatedUnion` rejects it, and
103
- * each arm carries only the fields it needs.
104
- */
105
- const checkVerdictBaseline = (value, what, fail) => {
106
- const record = requireRecord(value, what, fail);
107
- const kind = requireEnum(record.kind, VERDICT_BASELINE_KINDS, `${what}.kind`, fail);
108
- switch (kind) {
109
- case "term": {
110
- requireNonEmptyString(record.term, `${what}.term`, fail);
111
- break;
112
- }
113
- case "presence": {
114
- return;
115
- }
116
- case "seed": {
117
- requireNonEmptyString(record.variantId, `${what}.variantId`, fail);
118
- return;
119
- }
120
- default: {
121
- // `image` — the like-this-picture arm.
122
- requireEnum(record.source, VERDICT_IMAGE_SOURCES, `${what}.source`, fail);
123
- requireNullableNonEmptyString(record.variantId, `${what}.variantId`, fail);
124
- }
125
- }
126
- };
127
- /** `verdictDeviationSchema` — a named near-miss. */
128
- const checkVerdictDeviation = (value, what, fail) => {
129
- const record = requireRecord(value, what, fail);
130
- requireEnum(record.direction, VERDICT_DEVIATION_DIRECTIONS, `${what}.direction`, fail);
131
- requireNullableNumber(record.delta, `${what}.delta`, fail);
132
- requireNullableString(record.unit, `${what}.unit`, fail);
133
- requireNonEmptyString(record.label, `${what}.label`, fail);
134
- };
135
- /**
136
- * `attributeVerdictSchema` — nine fields plus the TWO cross-field invariants its
137
- * `.superRefine()` enforces. Those are relational rather than field-wise, which
138
- * is no obstacle to a hand-rolled validator: they are two conditionals, and the
139
- * schema's own doc calls them out as the reason a malformed cell can never reach
140
- * a consumer.
141
- */
142
- const checkAttributeVerdict = (value, what, fail) => {
143
- const record = requireRecord(value, what, fail);
144
- requireNonEmptyString(record.attributeId, `${what}.attributeId`, fail);
145
- const state = requireEnum(record.state, VERDICT_STATES, `${what}.state`, fail);
146
- requireEnum(record.strength, VERDICT_STRENGTHS, `${what}.strength`, fail);
147
- checkVerdictBaseline(record.baseline, `${what}.baseline`, fail);
148
- requireNullableString(record.value, `${what}.value`, fail);
149
- requireNullableBoundedNumber(record.confidence, `${what}.confidence`, fail, 0, 1);
150
- requireNullableString(record.sourceBasis, `${what}.sourceBasis`, fail);
151
- if (record.citation !== undefined) {
152
- const citation = requireRecord(record.citation, `${what}.citation`, fail);
153
- requireNonEmptyString(citation.citationId, `${what}.citation.citationId`, fail);
154
- requireNonEmptyString(citation.sourceType, `${what}.citation.sourceType`, fail);
155
- const count = requireNumber(citation.count, `${what}.citation.count`, fail);
156
- if (!Number.isInteger(count) || count <= 0) {
157
- throw fail(`${what}.citation.count must be a positive integer`);
158
- }
159
- }
160
- const { deviation } = record;
161
- if (deviation !== null) {
162
- checkVerdictDeviation(deviation, `${what}.deviation`, fail);
163
- }
164
- const trustGap = requireNullableEnum(record.trustGap, VERDICT_TRUST_GAPS, `${what}.trustGap`, fail);
165
- // superRefine invariant 1 — a deviation only describes a `deviated` cell.
166
- if (deviation !== null && state !== "deviated") {
167
- throw fail(`${what}.deviation is set on a cell whose state is "${state}", not "deviated"`);
168
- }
169
- // superRefine invariant 2 — a trust gap only explains an `unverified` cell.
170
- if (trustGap !== null && state !== "unverified") {
171
- throw fail(`${what}.trustGap is set on a cell whose state is "${state}", not "unverified"`);
172
- }
173
- };
174
- /** `candidateVerdictSchema` — the whole report card. */
175
- const checkCandidateVerdict = (value, what, fail) => {
176
- const record = requireRecord(value, what, fail);
177
- const cells = requireArray(record.cells, `${what}.cells`, fail);
178
- cells.forEach((cell, index) => {
179
- checkAttributeVerdict(cell, `${what}.cells[${index}]`, fail);
180
- });
181
- requireEnum(record.grade, VERDICT_GRADES, `${what}.grade`, fail);
182
- requireNullableBoundedNumber(record.minConfidence, `${what}.minConfidence`, fail, 0, 1);
42
+ requireNullableNumber(record.image, `${what}.image`, fail);
183
43
  };
184
44
  // ── The item ─────────────────────────────────────────────────────────────────
185
45
  /**
@@ -230,24 +90,6 @@ export const checkSearchResultItem = (value, what, fail) => {
230
90
  requireOptionalBoolean(record.sampleable, `${what}.sampleable`, fail);
231
91
  requireOptionalBoolean(record.belowFloor, `${what}.belowFloor`, fail);
232
92
  requireOptionalBoolean(record.admittedOnColour, `${what}.admittedOnColour`, fail);
233
- if (record.siblingCount !== undefined) {
234
- requireIntegerAtLeast(record.siblingCount, `${what}.siblingCount`, fail, 1);
235
- }
236
- if (record.variants !== undefined) {
237
- const variants = requireArray(record.variants, `${what}.variants`, fail);
238
- variants.forEach((variant, index) => {
239
- checkCollapsedVariant(variant, `${what}.variants[${index}]`, fail);
240
- });
241
- }
242
- if (record.visualMeanings !== undefined) {
243
- const meanings = requireArray(record.visualMeanings, `${what}.visualMeanings`, fail);
244
- meanings.forEach((meaning, index) => {
245
- checkVisualMeaning(meaning, `${what}.visualMeanings[${index}]`, fail);
246
- });
247
- }
248
- if (record.verdict !== undefined) {
249
- checkCandidateVerdict(record.verdict, `${what}.verdict`, fail);
250
- }
251
93
  };
252
94
  /** Prove every entry of a `results[]` array is a {@link SearchResultItem}. */
253
95
  export const checkSearchResultItems = (items, what, fail) => {
@@ -29,8 +29,9 @@ export type SpecFacet = {
29
29
  isArray: boolean;
30
30
  /**
31
31
  * How this facet is queried. `token` — pick a value from `values` and send it
32
- * as `attribute:value`. `numeric` — `values` is EMPTY and `range` carries the
33
- * span; send inclusive bounds via `specRangeCriterion` instead.
32
+ * as a `{field: attributeId, op: "in", values: [...]}` filter leaf.
33
+ * `numeric` — `values` is EMPTY and `range` carries the span; send inclusive
34
+ * bounds as `{field: attributeId, op: "gte"/"lte", value}` leaves instead.
34
35
  */
35
36
  kind: "token" | "numeric" | (string & {});
36
37
  brands: number;
@@ -45,9 +46,3 @@ export type SpecFacetsListResponse = {
45
46
  count: number;
46
47
  facets: SpecFacet[];
47
48
  };
48
- export type SpecFacetsDistributionResponse = {
49
- mode: "distribution";
50
- attributeId: string;
51
- count: number;
52
- values: SpecFacetValue[];
53
- };
package/spec-types.d.ts CHANGED
@@ -16,16 +16,17 @@
16
16
  * - `client.search({ filters: [...] })` — spec gates layered onto a RANKED
17
17
  * (colour / visual / lexical) search. Reach for it when relevance still
18
18
  * matters: "something like this seed, but Class A".
19
- * - `client.bySpec(...)` — the value-anchored, SPEC-ONLY primitive, which takes
20
- * the same criteria as `attribute:value` strings (build them with the helpers
21
- * in `./specifications`). Reach for it when the spec is the whole question and
22
- * ranking would be noise.
19
+ * - `client.listVariants({ filters })` — the value-anchored, SPEC-ONLY
20
+ * primitive, which takes a `{field: attributeId, op, ...}` filter leaf
21
+ * (build one term with the helpers in `./specifications`, or a plain
22
+ * object literal for anything else). Reach for it when the spec is the
23
+ * whole question and ranking would be noise.
23
24
  *
24
25
  * Discover the tokens before you spend them: `client.specificationFacets()` for
25
26
  * what is facetable at all, `client.attributeValues()` for one attribute's
26
- * values, `client.resolveAttribute()` / `client.resolveAttributeValue()` to turn
27
- * a phrase into a canonical pair. A hand-guessed token returns an honest empty
28
- * answer, which is the hardest kind of bug to see.
27
+ * values, `client.listAttributes({ query })` to turn a phrase into a canonical
28
+ * attribute id. A hand-guessed token returns an honest empty answer, which is
29
+ * the hardest kind of bug to see.
29
30
  */
30
31
  /**
31
32
  * The virtual attribute id that matches a certification PROGRAMME wherever it
@@ -57,17 +58,23 @@ export type CertificationProgrammeAttributeId = typeof CERTIFICATION_PROGRAMME_A
57
58
  * A term-equality spec criterion: the attribute must carry this canonical value,
58
59
  * e.g. `{ attributeId: "fire_classifications", term: "astm_e84_class_a" }`.
59
60
  * Discover valid `attributeId`/`term` pairs via `client.specificationFacets()`
60
- * and `client.attributeValues()`, or resolve free text with
61
- * `client.resolveAttribute()`.
61
+ * and `client.attributeValues()`, or find an attribute from free text with
62
+ * `client.listAttributes({ query })`.
62
63
  */
63
64
  export type SpecTermFilter = {
64
65
  attributeId: string;
65
66
  term: string;
66
67
  };
67
- /** A negative spec criterion; absence of a value remains unproven. */
68
+ /**
69
+ * A negative spec criterion. `whenUnstated` (absent = `"drop"`) controls what an
70
+ * UNKNOWN effective value does: `"drop"` requires a known value, so a variant
71
+ * that never states the attribute does not pass; `"pass"` admits it — a variant
72
+ * only fails by STATING the excluded term.
73
+ */
68
74
  export type SpecExclusionFilter = {
69
75
  attributeId: string;
70
76
  excludesTerm: string;
77
+ whenUnstated?: "drop" | "pass";
71
78
  };
72
79
  /**
73
80
  * A presence spec criterion: the attribute must have ANY accepted value —
package/spec-types.js CHANGED
@@ -16,16 +16,17 @@
16
16
  * - `client.search({ filters: [...] })` — spec gates layered onto a RANKED
17
17
  * (colour / visual / lexical) search. Reach for it when relevance still
18
18
  * matters: "something like this seed, but Class A".
19
- * - `client.bySpec(...)` — the value-anchored, SPEC-ONLY primitive, which takes
20
- * the same criteria as `attribute:value` strings (build them with the helpers
21
- * in `./specifications`). Reach for it when the spec is the whole question and
22
- * ranking would be noise.
19
+ * - `client.listVariants({ filters })` — the value-anchored, SPEC-ONLY
20
+ * primitive, which takes a `{field: attributeId, op, ...}` filter leaf
21
+ * (build one term with the helpers in `./specifications`, or a plain
22
+ * object literal for anything else). Reach for it when the spec is the
23
+ * whole question and ranking would be noise.
23
24
  *
24
25
  * Discover the tokens before you spend them: `client.specificationFacets()` for
25
26
  * what is facetable at all, `client.attributeValues()` for one attribute's
26
- * values, `client.resolveAttribute()` / `client.resolveAttributeValue()` to turn
27
- * a phrase into a canonical pair. A hand-guessed token returns an honest empty
28
- * answer, which is the hardest kind of bug to see.
27
+ * values, `client.listAttributes({ query })` to turn a phrase into a canonical
28
+ * attribute id. A hand-guessed token returns an honest empty answer, which is
29
+ * the hardest kind of bug to see.
29
30
  */
30
31
  /**
31
32
  * The virtual attribute id that matches a certification PROGRAMME wherever it
@@ -1,6 +1,12 @@
1
1
  import type { CertificationProgrammeFilter, SpecExclusionFilter, SpecPresenceFilter, SpecTermFilter } from "./spec-types.js";
2
- /** A hard criterion excluding one known canonical value while retaining only variants with known values. */
3
- export declare const excludeSpec: (attributeId: string, excludesTerm: string) => SpecExclusionFilter;
2
+ /**
3
+ * A hard criterion excluding one canonical value.
4
+ *
5
+ * By default retains only variants with a KNOWN value (unstated does not
6
+ * pass). Pass `whenUnstated: "pass"` to admit unstated variants too — only a
7
+ * variant that STATES the excluded term is dropped.
8
+ */
9
+ export declare const excludeSpec: (attributeId: string, excludesTerm: string, whenUnstated?: "drop" | "pass") => SpecExclusionFilter;
4
10
  /**
5
11
  * A term-equality criterion for `search({ filters })` — the attribute must
6
12
  * carry this canonical value.
@@ -24,61 +30,13 @@ export declare const hasSpec: (attributeId: string) => SpecPresenceFilter;
24
30
  * virtual attribute so a programme matches wherever it landed at ingest
25
31
  * (compliance / sustainability / material-health / VOC buckets).
26
32
  *
27
- * The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
28
- * server resolves it to its canonical programme token before matching — or a
29
- * canonical token directly (e.g. `greenguard_certificate`,
30
- * `has_greenguard_gold_certification`).
33
+ * The value is a canonical programme token (e.g. `greenguard_certificate`,
34
+ * `has_greenguard_gold_certification`). The one search matches tokens on its
35
+ * index and does not resolve display names, so a name such as
36
+ * "GREENGUARD Gold" is a 400 naming the term.
31
37
  *
32
38
  * @example
33
39
  * // "find a GREENGUARD-certified alternative"
34
- * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
40
+ * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("has_greenguard_gold_certification")] });
35
41
  */
36
42
  export declare const certificationProgramme: (programme: string) => CertificationProgrammeFilter;
37
- /**
38
- * An `attribute:value` criterion string for `bySpec`. The `bySpec` route splits
39
- * on the FIRST `:`, so a value may itself contain colons; the attribute id may
40
- * not.
41
- *
42
- * @example
43
- * client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
44
- */
45
- export declare const specCriterion: (attributeId: string, value: string) => string;
46
- /**
47
- * An `attribute:min..max` RANGE criterion string for `bySpec`, for a
48
- * NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
49
- * `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
50
- *
51
- * BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
52
- * because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
53
- * and because inclusive-both-ends means adjacent ranges overlap at the join
54
- * rather than leaving a gap that hides products.
55
- *
56
- * At least one bound is REQUIRED: a range with neither is a presence check, and
57
- * this throws rather than sending a criterion the server would have to refuse.
58
- * Discover which attributes take a range — and the span they actually occupy —
59
- * from `specificationFacets()`: those are the facets with `kind: "numeric"`.
60
- *
61
- * @example
62
- * // light paints, every brand at once
63
- * client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
64
- * @example
65
- * // a mid-tone band
66
- * specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
67
- */
68
- export declare const specRangeCriterion: (attributeId: string, bounds: {
69
- min?: number;
70
- max?: number;
71
- }) => string;
72
- /**
73
- * An `attribute:value` criterion string for `bySpec` that filters by
74
- * certification PROGRAMME across all four certification buckets.
75
- *
76
- * The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
77
- * server-side to its canonical programme token, or a canonical token directly
78
- * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
79
- *
80
- * @example
81
- * // every brand's GREENGUARD-Gold materials
82
- * client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
83
- */
84
- export declare const certificationProgrammeCriterion: (programme: string) => string;
package/specifications.js CHANGED
@@ -1,22 +1,32 @@
1
1
  /**
2
- * Zero-dependency builders for the specification surface. Turn a plain-language
3
- * spec intent into a typed criterion the SDK's two spec operations accept:
4
- *
5
- * - `search({ filters })` takes {@link SpecFilter} objects — a spec gate layered
6
- * onto a ranked (colour / visual / lexical) search. Build them with
7
- * {@link specTerm}, {@link hasSpec}, and {@link certificationProgramme}.
8
- * - `bySpec(specs)` takes `attribute:value` strings — the value-anchored,
9
- * spec-only primitive. Build them with {@link specCriterion} and
10
- * {@link certificationProgrammeCriterion}.
2
+ * Zero-dependency builders for the specification surface. Turn a
3
+ * plain-language spec intent into a typed {@link SpecFilter} `search({
4
+ * filters })` accepts: hard token sets on the one search's index, run in the
5
+ * same query. Build them with {@link specTerm}, {@link hasSpec}, and
6
+ * {@link certificationProgramme}.
11
7
  *
12
8
  * The point of these helpers: Desk (and any spec-driven caller) never has to
13
- * remember that a certification programme lives under a VIRTUAL attribute, nor
14
- * hand-format the `attribute:value` string the `bySpec` route splits on its
15
- * first `:`.
9
+ * remember that a certification programme lives under a VIRTUAL attribute.
10
+ *
11
+ * A hard specification across every brand (formerly `bySpec`, built with
12
+ * `specCriterion`/`specRangeCriterion`) is now `list_variants`'s `filters`:
13
+ * `{field: attributeId, op: "in", values: [...]}` for a term,
14
+ * `{field: attributeId, op: "gte"/"lte"/etc, value}` for a number — a plain
15
+ * object literal needs no encoder (MG-2222).
16
16
  */
17
17
  import { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID } from "./spec-types.js";
18
- /** A hard criterion excluding one known canonical value while retaining only variants with known values. */
19
- export const excludeSpec = (attributeId, excludesTerm) => ({ attributeId, excludesTerm });
18
+ /**
19
+ * A hard criterion excluding one canonical value.
20
+ *
21
+ * By default retains only variants with a KNOWN value (unstated does not
22
+ * pass). Pass `whenUnstated: "pass"` to admit unstated variants too — only a
23
+ * variant that STATES the excluded term is dropped.
24
+ */
25
+ export const excludeSpec = (attributeId, excludesTerm, whenUnstated) => ({
26
+ attributeId,
27
+ excludesTerm,
28
+ ...(whenUnstated === undefined ? {} : { whenUnstated }),
29
+ });
20
30
  /**
21
31
  * A term-equality criterion for `search({ filters })` — the attribute must
22
32
  * carry this canonical value.
@@ -43,70 +53,16 @@ export const hasSpec = (attributeId) => ({
43
53
  * virtual attribute so a programme matches wherever it landed at ingest
44
54
  * (compliance / sustainability / material-health / VOC buckets).
45
55
  *
46
- * The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
47
- * server resolves it to its canonical programme token before matching — or a
48
- * canonical token directly (e.g. `greenguard_certificate`,
49
- * `has_greenguard_gold_certification`).
56
+ * The value is a canonical programme token (e.g. `greenguard_certificate`,
57
+ * `has_greenguard_gold_certification`). The one search matches tokens on its
58
+ * index and does not resolve display names, so a name such as
59
+ * "GREENGUARD Gold" is a 400 naming the term.
50
60
  *
51
61
  * @example
52
62
  * // "find a GREENGUARD-certified alternative"
53
- * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
63
+ * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("has_greenguard_gold_certification")] });
54
64
  */
55
65
  export const certificationProgramme = (programme) => ({
56
66
  attributeId: CERTIFICATION_PROGRAMME_ATTRIBUTE_ID,
57
67
  term: programme,
58
68
  });
59
- /**
60
- * An `attribute:value` criterion string for `bySpec`. The `bySpec` route splits
61
- * on the FIRST `:`, so a value may itself contain colons; the attribute id may
62
- * not.
63
- *
64
- * @example
65
- * client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
66
- */
67
- export const specCriterion = (attributeId, value) => `${attributeId}:${value}`;
68
- /**
69
- * An `attribute:min..max` RANGE criterion string for `bySpec`, for a
70
- * NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
71
- * `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
72
- *
73
- * BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
74
- * because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
75
- * and because inclusive-both-ends means adjacent ranges overlap at the join
76
- * rather than leaving a gap that hides products.
77
- *
78
- * At least one bound is REQUIRED: a range with neither is a presence check, and
79
- * this throws rather than sending a criterion the server would have to refuse.
80
- * Discover which attributes take a range — and the span they actually occupy —
81
- * from `specificationFacets()`: those are the facets with `kind: "numeric"`.
82
- *
83
- * @example
84
- * // light paints, every brand at once
85
- * client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
86
- * @example
87
- * // a mid-tone band
88
- * specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
89
- */
90
- export const specRangeCriterion = (attributeId, bounds) => {
91
- const { min, max } = bounds;
92
- if (min === undefined && max === undefined) {
93
- throw new Error(`specRangeCriterion("${attributeId}") needs a min, a max, or both — a range with neither would match every recorded value. Use hasSpec() for a presence check.`);
94
- }
95
- if (min !== undefined && max !== undefined && min > max) {
96
- throw new Error(`specRangeCriterion("${attributeId}") is inverted (min ${min} > max ${max}) and can only ever match nothing.`);
97
- }
98
- return `${attributeId}:${min ?? ""}..${max ?? ""}`;
99
- };
100
- /**
101
- * An `attribute:value` criterion string for `bySpec` that filters by
102
- * certification PROGRAMME across all four certification buckets.
103
- *
104
- * The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
105
- * server-side to its canonical programme token, or a canonical token directly
106
- * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
107
- *
108
- * @example
109
- * // every brand's GREENGUARD-Gold materials
110
- * client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
111
- */
112
- export const certificationProgrammeCriterion = (programme) => specCriterion(CERTIFICATION_PROGRAMME_ATTRIBUTE_ID, programme);
@@ -12,11 +12,10 @@
12
12
  * `mg.searchVocabulary()` covers attribute/value tokens. Suggestions are a page
13
13
  * of the best matches, never a census of matching entities.
14
14
  */
15
- import type { CorpusScope, CorpusScopeReceipt } from "./corpus-scope-types.js";
16
15
  /** Which kind of entity a suggestion is — and therefore what `refId` means. */
17
16
  export type SuggestKind = "product" | "brand" | "category";
18
17
  /** How the query met the suggestion's label. Strongest first. */
19
- export type SuggestMatch = "prefix" | "word" | "contains" | "fuzzy";
18
+ export type SuggestMatch = "prefix" | "word" | "fuzzy";
20
19
  /** One ranked typeahead suggestion, carrying the identity it resolves to. */
21
20
  export type Suggestion = {
22
21
  kind: SuggestKind;
@@ -36,17 +35,15 @@ export type Suggestion = {
36
35
  * Opaque, and null when none was picked — not a product without variants.
37
36
  */
38
37
  variantId: string | null;
39
- /** Variants behind this entity IN MATERIALGRAPH — a coverage figure. */
38
+ /** How many of this entity's variants the search index holds: a coverage figure. */
40
39
  variantCount: number;
41
40
  };
42
41
  /** The suggest response: the query as interpreted, plus the ranked suggestions. */
43
42
  export type SuggestResponse = {
44
- /** The normalised query the corpus was matched against. */
43
+ /** The typed words the search index was matched against. */
45
44
  query: string;
46
- /** Ranked best-match-first. Empty when nothing came close — a miss, not an error. */
45
+ /** Ranked best-match-first. Empty when nothing came close: a miss, not an error. */
47
46
  suggestions: Suggestion[];
48
- /** Present only when a narrowing governed corpus was applied. */
49
- corpusScope?: CorpusScopeReceipt;
50
47
  };
51
48
  /** Options for {@link MaterialGraphClient.suggest}. */
52
49
  export type SuggestOptions = {
@@ -54,8 +51,6 @@ export type SuggestOptions = {
54
51
  limit?: number;
55
52
  /** Restrict to these kinds. Omit for all of them. */
56
53
  kind?: SuggestKind | SuggestKind[];
57
- /** Governed corpus membership; absent means the whole graph. */
58
- corpusScope?: CorpusScope;
59
54
  };
60
55
  /**
61
56
  * Shape the query string for a suggest call.
@@ -69,5 +64,4 @@ export declare const suggestQuery: (q: string, options?: SuggestOptions) => {
69
64
  q: string;
70
65
  limit?: number;
71
66
  kind?: string;
72
- corpusScope?: CorpusScope;
73
67
  };
package/suggest-types.js CHANGED
@@ -24,5 +24,5 @@ export const suggestQuery = (q, options) => {
24
24
  const kind = Array.isArray(options?.kind)
25
25
  ? options.kind.join(",")
26
26
  : options?.kind;
27
- return { corpusScope: options?.corpusScope, kind, limit: options?.limit, q };
27
+ return { kind, limit: options?.limit, q };
28
28
  };