@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,86 +1,75 @@
1
- /** samplizeCatalogue admits published US Samplize paint colours, regardless of stock. */
2
1
  /**
3
2
  * @module search-request-types
4
- * The two request shapes for the `/search` surface — the POST JSON body and the
3
+ * The two request shapes for the `/search` surface: the POST JSON body and the
5
4
  * GET query-string form. Split out of `types.ts` so neither file outgrows the
6
5
  * repo's source-size gate; every name here is re-exported from `types.ts`, so
7
6
  * no import path changes.
8
7
  */
9
8
  import type { CorpusScope } from "./corpus-scope-types.js";
10
9
  import type { Cursor, VariantId } from "./identifiers.js";
11
- import type { SearchFilter } from "./search-refinement-types.js";
12
- import type { ColourInput, OpenEnum, SearchImageInput } from "./types.js";
10
+ import type { OpenEnum, SearchImageInput } from "./types.js";
13
11
  /**
14
- * The POST `/search` JSON body — the `searchMaterials` contract. Every field is
15
- * optional; supply at least one signal (`text`, `colours`, `mood`, `similarTo`,
16
- * or `image`). Passed through to the server verbatim.
12
+ * One attribute filter the one search runs as a hard token set on its index
13
+ * (ADR 0031, MG-2158). Entries on one attribute are OR'd; entries on different
14
+ * attributes AND together.
15
+ *
16
+ * - `term`: the value must carry this canonical term.
17
+ * - `terms`: any one of these terms satisfies it.
18
+ * - `excludesTerm`: the value must not carry this term. A material that states
19
+ * nothing for the attribute passes.
20
+ * - `hasValue: true`: any stated value.
21
+ * - `min` and/or `max`: an inclusive numeric range, on a numeric attribute
22
+ * (`"nrc_rating"`), a dimensions lens in millimetres
23
+ * (`"dimensions.thickness_mm"`, `"dimensions.short_side_mm"`) or a
24
+ * composition share on 0 to 1 (`"composition_weighted.wool"`). It is read as
25
+ * the number bands wholly inside it, so a bound between two band edges is
26
+ * read inward; a range no band fits inside is a 400.
27
+ *
28
+ * `attributeId` is an attribute the index carries tokens for, or
29
+ * `"certification_programme"` for any certification programme. Terms are
30
+ * canonical dictionary terms, not display names. Numeric attributes take
31
+ * `hasValue` or a range. An attribute or term the index does not carry is a
32
+ * 400 naming it.
33
+ */
34
+ export type SearchIndexFilter = {
35
+ attributeId: string;
36
+ term: string;
37
+ } | {
38
+ attributeId: string;
39
+ terms: string[];
40
+ } | {
41
+ attributeId: string;
42
+ excludesTerm: string;
43
+ } | {
44
+ attributeId: string;
45
+ hasValue: true;
46
+ } | {
47
+ attributeId: string;
48
+ min?: number;
49
+ max?: number;
50
+ };
51
+ /**
52
+ * The POST `/search` JSON body (ADR 0031): the words, or a look-alike seed, and
53
+ * the page wanted. `category`, `filters`, `corpusScope`, `colour` and `brands`
54
+ * become fields of the query's plan and run in the same index query. The order
55
+ * is the index's own, so nothing here sets weights, a blend or a sort. Send
56
+ * `text`, `similarTo`, `image`, `colour`, `category`, `filters` or `brands`; a
57
+ * corpus scope alone does not say what to find. Any other field is a 400
58
+ * naming it.
17
59
  */
18
60
  export type SearchInput = {
19
- text?: string;
20
- textModes?: OpenEnum<"visual" | "lexical">[];
21
- colours?: ColourInput[];
22
- colourLogic?: OpenEnum<"all" | "any">;
23
- colourTolerance?: OpenEnum<"tight" | "normal" | "loose">;
24
- colourAppearance?: {
25
- lightness?: {
26
- min: number;
27
- max: number;
28
- };
29
- warmth?: {
30
- min: number;
31
- max: number;
32
- };
33
- };
34
- mood?: string;
35
61
  /**
36
- * Re-order the final scored results. `relevance` (default) keeps the selected
37
- * mode's primary score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
38
- * ramps; `name` (A→Z), `name_desc` (Z→A) and `recent` (newest first) are the
39
- * catalogue orders — the same three `/search/browse` walks the corpus in, so
40
- * one word means one order on both lanes.
62
+ * Free text ("oak flooring", "sage green velvet", "Kvadrat wool"). Brands,
63
+ * categories, colours and attributes are read from the words unless
64
+ * `brands`, `category` or `colour` says so instead; a corrected word is
65
+ * reported under `correctedQuery`.
41
66
  */
42
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
43
- /**
44
- * Sort along ONE attribute's declared value ladder — a SEPARATE axis from
45
- * `sort`, which stays the closed relevance / colour / catalogue vocabulary.
46
- *
47
- * `direction` defaults to `"asc"`. Sent together with `sort`, `orderBy` wins
48
- * and the response `note` says so — a UI holding both controls can send both.
49
- *
50
- * Ordering is DECLARED, never inferred from how a value is spelled: an
51
- * attribute with no stated ladder is a 400 carrying the orderable set in
52
- * `orderable_attributes`, not a quiet fall back to relevance. A material whose
53
- * value is off the ladder, or which carries no value at all, sorts LAST in
54
- * both directions — it is still a real material, it just has no rung.
55
- */
56
- orderBy?: {
57
- attributeId: string;
58
- direction?: OpenEnum<"asc" | "desc">;
59
- };
60
- /**
61
- * Attribute ids whose resolved value should ride each result under
62
- * `attributes` — so a page you filtered by `price_tier` can DISPLAY the tier
63
- * without a second call per material. Up to 8.
64
- *
65
- * Three response states, all different: no `attributes` key means you did not
66
- * ask, a key with `null` means nothing is on record for that material, and
67
- * anything else is the value. An unknown attribute id is a 400 naming it.
68
- */
69
- returnAttributes?: string[];
67
+ text?: string;
68
+ /** A query image (URL or base64); `text`, when also given, narrows it. */
69
+ image?: SearchImageInput;
70
70
  /**
71
- * Seed a visual search from an existing variant's image (`variantId`), from a
72
- * Material Bank SKU (`materialBankId`), or from a pre-computed image vector
73
- * (`vector`).
74
- *
75
- * `materialBankId` exists so a caller already holding a SKU does not have to
76
- * ask us to translate it before it can ask us anything useful — one call, not
77
- * two. It resolves to the variant server-side and then behaves identically to
78
- * the `{ variantId }` form, seed verdicts included. A SKU that names no
79
- * variant is a 400 carrying the offending value: never an empty result set,
80
- * never a silent fall back to unseeded search.
81
- *
82
- * The `{ vector }` form carries no seed variant, so it is the only one
83
- * `verdicts` cannot diff a candidate against.
71
+ * Materials that LOOK like a seed: an existing variant, a Material Bank SKU
72
+ * (an unknown SKU is a 400 naming it), or a pre-computed image vector.
84
73
  */
85
74
  similarTo?: {
86
75
  variantId: VariantId;
@@ -90,156 +79,84 @@ export type SearchInput = {
90
79
  vector: string;
91
80
  };
92
81
  /**
93
- * Canonical variants that must not appear in the returned page. The server
94
- * applies these exclusions before the page limit, so consumers can request
95
- * unseen results without shortening an already-bounded page client-side.
96
- * Maximum 256; ids are opaque.
82
+ * A hard category gate: public category-group or product-type names, or
83
+ * taxonomy keys. It replaces the category the words were read as. An
84
+ * unknown value is a 400 listing every valid name.
97
85
  */
98
- excludeVariantIds?: VariantId[];
99
- image?: SearchImageInput;
100
86
  category?: {
101
- boost?: string;
102
- filter?: string[];
87
+ filter: string[];
103
88
  };
89
+ /** Hard attribute filters, 1 to 24. See {@link SearchIndexFilter}. */
90
+ filters?: SearchIndexFilter[];
104
91
  /**
105
- * Optional multipliers for the selected mode's one ordering signal. At most
106
- * one is applied: image for image/image-text, lexical for lexical/browse, and
107
- * colour for colour mode. Non-mode keys are accepted for compatibility but
108
- * cannot activate secondary scoring.
92
+ * Order the page by a colour: a `hex`, or `variantId` for the colour of an
93
+ * existing Variant. The page is the colour's family, nearest first by OKLab
94
+ * distance; it replaces any colour read from `text`. A Variant with no colour
95
+ * on the index is a 400.
109
96
  */
110
- weights?: {
111
- image?: number;
112
- category?: number;
113
- colour?: number;
114
- lexical?: number;
115
- /** Compatibility field; seeded text does not activate secondary scoring. */
116
- text?: number;
97
+ colour?: {
98
+ hex: string;
99
+ } | {
100
+ variantId: VariantId;
117
101
  };
118
- filters?: SearchFilter[];
119
- /**
120
- * SOFT spec criteria (rank-never-exclude). Same shape as `filters`; a preference
121
- * never removes a result — it adds a `preferred` cell to each hit's `verdict` and
122
- * demotes a failed/missing preferred hit WITHIN its page only.
123
- */
124
- preferences?: SearchFilter[];
125
- /** Results per page (default 60, max 60), never a bound on the match set. */
126
- limit?: number;
127
- /** The previous response's `cursor`, passed back verbatim. Never synthesise one. */
128
- cursor?: Cursor;
129
- explain?: boolean;
130
- /** Public POST defaults to distinct products; send `"none"` for individual variants. */
131
- collapse?: OpenEnum<"none" | "product">;
132
102
  /**
133
- * Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools.
134
- * Without it the response states NO total — and a page size is not one.
103
+ * Makers the page must come from: canonical brand ids, exact slugs
104
+ * ("sherwin-williams") or exact display names ("Sherwin-Williams"), up to
105
+ * 50. They replace the makers `text` was read as. An empty list is the
106
+ * same request as omitting it — every brand. An unrecognised value is a
107
+ * 400 naming it; a display name shared by more than one brand is a 400
108
+ * naming the exact slugs to choose between; a brand MaterialGraph holds no
109
+ * materials under is a 400 naming a populated sibling when one exists.
135
110
  */
136
- count?: boolean;
111
+ brands?: string[];
137
112
  /**
138
- * Opt-in per-query facet counts (MG-1010): up to 10 facet ids — `"brand"`,
139
- * `"category"`, or spec attribute ids — and the response carries a `facets`
140
- * block counting each facet's values WITHIN this query's match set. An
141
- * unknown id is a 400 naming it; a known but uncountable id lands in
142
- * `facets.absent`. Absent = no `facets` in the response, no counting cost.
113
+ * Attribute ids (at most 8) whose resolved value should ride each result
114
+ * under `attributes`, keyed by attribute id. Not a plan field: it changes
115
+ * no ordering and plays no part in the cursor. An attribute you did not
116
+ * ask for is not a key on `attributes` at all; one you asked for that a
117
+ * result carries no accepted value for is `null`; anything else is the
118
+ * value. An unknown attribute id is a 400 naming it.
143
119
  */
144
- facetCounts?: string[];
145
- /**
146
- * Opaque five-minute receipt from a prior pool-derived faceted search.
147
- * Resubmitting it may only narrow that exact finalized pool.
148
- */
149
- refinementToken?: string;
150
- /** Only return materials with at least one catalog image. Default false (full catalog). */
151
- hasImages?: boolean;
152
- /** Require a recorded accepted sampleable=true fact; unknown and false are excluded. */
153
- sampleableOnly?: boolean;
154
- /**
155
- * Opt-in seed-relative verdict cells (MG-629). Only meaningful with
156
- * `similarTo: { variantId }`: each hit's `verdict` gains `preferred` cells
157
- * diffing its comparable attributes (numeric + a `colour` axis) against the
158
- * seed's, with directional deviations. Inert on every other seed form.
159
- */
160
- verdicts?: boolean;
161
- /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
162
- view?: OpenEnum<"full" | "card">;
163
- /**
164
- * Budget profile for the search primitive.
165
- *
166
- * `"search"` (default) is the full engine. `"typeahead"` is the as-you-type
167
- * profile: lexical-only with prefix match on the final token, forced
168
- * `view: "card"`, `limit` capped at 24, and no mood/colour detection tiers,
169
- * semantic fallback, secondary scoring, refinement suggestions,
170
- * counts, or meanings. `textModes` is ignored under typeahead.
171
- */
172
- intent?: OpenEnum<"search" | "typeahead">;
120
+ returnAttributes?: string[];
173
121
  /**
174
- * Which corpus to answer from.
175
- *
176
- * **ABSENT means the WHOLE GRAPH** — every material MaterialGraph knows,
177
- * orderable or not. Absence is never a narrower default, so a caller that
178
- * says nothing gets materials it may not be able to buy.
179
- * Three transaction-oriented scopes gate on governed CHANNEL membership:
180
- * `"materialBankNaOrderable"` (Material Bank North America,
181
- * ~277,000 materials), `"materialBankEuOrderable"` (Material Bank Europe,
182
- * ~65,000) and `"designShopOrderable"` (DesignShop, ~34,000, with the
183
- * products DesignShop hides from browse excluded).
184
- * `"materialBankNaAndCatalogue"` is a governed discovery corpus: the single
185
- * producer-side union of Material Bank North America and Catalogue editorial
186
- * membership. It is not a buyability claim, and a catalogue reference alone
187
- * does not establish membership.
188
- *
189
- * `"materialBankOrderable"` is NOT one of them, despite the name. It selects
190
- * materials carrying a Material Bank identity — a PROVENANCE signal, not a
191
- * storefront — and admits about 98% of the graph. Use it to exclude the
192
- * crawl-sourced brands that were never in the Material Bank feed; if you mean
193
- * orderable, use one of the channel-backed scopes above.
194
- *
195
- * A scope answers membership in its named distribution or visibility
196
- * surface; the token-specific description states whether that means
197
- * transaction or editorial inclusion. It never answers what is SUITABLE:
198
- * scope on channel, then refine on sector.
199
- *
200
- * The scope is applied INSIDE the query: `limit` fills with eligible rows,
201
- * `counts` describe the SCOPED corpus, and the response echoes the scope.
202
- * That is what post-filtering a page cannot give you — a client-side filter
203
- * shortens the page exactly when the query drifts toward the corpus beyond
204
- * the Material Bank feed, which is unpredictable from outside.
122
+ * Which corpus to answer from, applied inside the index query. ABSENT (or
123
+ * `"all"`) means the whole graph. `"materialBankOrderable"` is a provenance
124
+ * signal (a Material Bank identity), not a storefront; the channel-backed
125
+ * scopes (`"materialBankNaOrderable"`, `"materialBankEuOrderable"`,
126
+ * `"designShopOrderable"`) answer where a material can be ordered,
127
+ * `"materialBankNaAndCatalogue"` is a governed discovery corpus (the union
128
+ * of Material Bank North America and Catalogue editorial membership, not a
129
+ * buyability claim), and `"samplizeCatalogue"` admits published US Samplize
130
+ * paint colours, regardless of stock.
205
131
  */
206
132
  corpusScope?: CorpusScope;
133
+ /** Products per page (max 60). Absent, the search's own page size. */
134
+ limit?: number;
207
135
  /**
208
- * OPS/AUDIT escape hatch — do NOT use for normal search. Default behaviour
209
- * (absent or false) excludes sampling-artifact products: multi-swatch sample
210
- * cards, which are orderable samples and never specifiable materials.
136
+ * The previous response's `cursor`, passed back verbatim. A cursor from
137
+ * another query (other words, filters or scope) is a 409.
211
138
  */
212
- includeSamplingArtifacts?: boolean;
139
+ cursor?: Cursor;
140
+ /** Add `timings` and `explain.plan`, the plan the search ran on. */
141
+ explain?: boolean;
142
+ /** Result projection: `"full"` (default) or the lean `"card"` grid form. */
143
+ view?: OpenEnum<"full" | "card">;
213
144
  };
214
- /** Query-string params for the GET `/search` form. */
145
+ /**
146
+ * Query-string params for the GET `/search` form. `category` and `filters`
147
+ * have no query-string form; send them through `search()`.
148
+ */
215
149
  export type SearchGetParams = {
216
150
  q?: string;
217
- colours?: string;
218
- logic?: OpenEnum<"all" | "any">;
219
- mood?: string;
220
- /** Governed corpus applied inside retrieval; absent means the whole graph. */
221
- corpusScope?: CorpusScope;
222
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
223
- tol?: OpenEnum<"tight" | "normal" | "loose">;
224
- /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
151
+ /** Seed the search from this variant's image. Opaque; pass it through verbatim. */
225
152
  variantId?: VariantId;
153
+ /** Corpus applied inside the index query; absent means the whole graph. */
154
+ corpusScope?: CorpusScope;
226
155
  limit?: number;
227
- /** "0" or "1" — include per-signal `signals` on each result. */
228
- explain?: OpenEnum<"0" | "1">;
229
- /** "true" — opt in to a `counts.variants` feed total. */
230
- count?: OpenEnum<"true">;
231
- /** "true" — only return materials with at least one catalog image. */
232
- hasImages?: OpenEnum<"true">;
233
- /** Require a recorded accepted sampleable=true fact. */
234
- sampleableOnly?: OpenEnum<"true" | "false">;
235
- /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
156
+ /** The previous response's `cursor`, passed back verbatim. */
157
+ cursor?: Cursor;
158
+ /** Result projection: `"full"` (default) or the lean `"card"` grid form. */
236
159
  view?: OpenEnum<"full" | "card">;
237
- /**
238
- * One JSON OBJECT, e.g. `{"attributeId":"price_tier","direction":"desc"}` —
239
- * the `orderBy` field of the POST body, JSON-encoded because a query string
240
- * has no nested values. Same spelling as `filters=` on `/search/browse`.
241
- */
242
- orderBy?: string;
243
- /** Comma-separated attribute ids — the `returnAttributes` list. */
244
- returnAttributes?: string;
160
+ /** `"1"` adds `timings` and `explain.plan`. */
161
+ explain?: OpenEnum<"0" | "1">;
245
162
  };
@@ -1,280 +1,24 @@
1
1
  /**
2
2
  * @module sdk/search-response-receipt-types
3
- * @description Inspectable routing, compatibility, and compound-constraint
4
- * receipts emitted by ordinary Search responses.
3
+ * @description The ranking source and the explain-only plan receipt a Search
4
+ * response carries.
5
5
  */
6
+ import type { SearchPlanReceipt } from "./search-index-plan-types.js";
6
7
  import type { OpenEnum } from "./types.js";
7
- /** Inspectable metadata for an opaque, principal-bound search refinement receipt. */
8
- export type SearchRefinementReceipt = {
9
- token: string;
10
- expiresAt: string;
11
- poolSize: number;
12
- schemaVersion: string;
13
- projectionVersion: number;
14
- };
15
- /** The primary ranking source a query resolved to and reports as its mode. */
16
- export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse" | "agent">;
17
- /**
18
- * Compatibility receipt for non-mode colour handling. Current one-signal
19
- * ordering reports it skipped at weight 0; colour may still be diagnostic or a
20
- * hard displayed-image constraint, but does not blend into score.
21
- */
22
- export type SearchRerankReport = {
23
- colour?: OpenEnum<"ok" | "failed" | "skipped">;
24
- colourWeight?: number;
25
- colourLabel?: string;
26
- };
27
- /**
28
- * A part of the search that can fail without stopping it.
29
- *
30
- * Coarse on purpose: a caller needs to know that something did not answer, not
31
- * which provider it was. OPEN, and it will grow — read it with a default
32
- * branch.
33
- */
34
- export type SearchDegradedPart = OpenEnum<"filter-coverage" | "term-coverage">;
35
- /**
36
- * Which parts of the search did not answer, on EVERY response (MG-1913).
37
- *
38
- * `parts: []` says no part reported a failure. It does NOT promise every check
39
- * ran: a check the request never needed is skipped rather than failed. ABSENT
40
- * means the response came from a server that does not report this, never that
41
- * nothing was degraded.
42
- */
43
- export type SearchDegraded = {
44
- parts: SearchDegradedPart[];
45
- };
46
- /** Stage counts retained when a search returns no results. */
47
- export type SearchEmptyOutcomeCounts = {
48
- retrieved: number;
49
- afterPrune: number;
50
- final: number;
51
- };
52
- /**
53
- * Why a search returned no results, on EVERY empty response (MG-1911).
54
- *
55
- * `proven-corpus-empty` is exhaustive and is the only member that supports
56
- * telling someone the catalogue holds nothing. `unproven-empty` is an honest
57
- * end to a search that ran without establishing that. `search-degraded` means
58
- * part of the search did not answer, so the empty page describes the runtime
59
- * rather than the catalogue, and the right response is to retry.
60
- *
61
- * Read it by `kind` with a default branch: this union has grown once and the
62
- * reason and basis enums are open.
63
- */
64
- export type SearchEmptyOutcome = {
65
- kind: "proven-corpus-empty";
66
- basis: OpenEnum<"spec-allowlist" | "brand-scope" | "brand-product-type">;
67
- exhaustive: true;
68
- sourceEmpty?: boolean | null;
69
- counts?: SearchEmptyOutcomeCounts;
70
- } | {
71
- kind: "unproven-empty";
72
- reason: OpenEnum<"allowlist-truncated" | "retrieval-window-not-exhaustive" | "source-empty" | "post-retrieval-rejected" | "pagination-empty" | "no-authoritative-empty-proof">;
73
- sourceEmpty?: boolean | null;
74
- counts?: SearchEmptyOutcomeCounts;
75
- } | {
76
- kind: "search-degraded";
77
- /** Which parts did not answer. Coarse: never a provider's name. */
78
- parts: SearchDegradedPart[];
79
- sourceEmpty?: boolean | null;
80
- counts?: SearchEmptyOutcomeCounts;
81
- };
82
- /** Explain-only coverage of substantive query terms in the lexical projection. */
83
8
  /**
84
- * Which of the query's substantive words the catalogue holds at all
85
- * (MG-1858), on every text-family response. `terms` names the words found in
86
- * no document; `checkedTerms` is how many were asked about; `noneMatched` is
87
- * true when no checked word is attested anywhere. `unavailable` means the read
88
- * failed and never means everything matched; `skipped` means this mode never
89
- * claimed to match words.
90
- */
91
- export type SearchTermCoverageReceipt = {
92
- status: OpenEnum<"covered" | "uncovered" | "unavailable" | "skipped">;
93
- terms: string[];
94
- checkedTerms: number;
95
- noneMatched: boolean;
96
- note?: string;
97
- };
98
- /** The Search workflow run a ledger receipt belongs to. */
99
- export type SearchRunRef = {
100
- contextId: string;
101
- runId: string;
102
- schemaVersion: 1;
103
- };
104
- /** One artifact a Search workflow step read or wrote. */
105
- export type SearchRunArtifactRef = {
106
- artifactId: string;
107
- kind: string;
108
- producedBy: string;
109
- runRef: SearchRunRef;
110
- schemaVersion: 1;
111
- };
112
- /** What one Search workflow step did: its reads, its write and its time. */
113
- export type SearchStageReceipt = {
114
- stageId: string;
115
- status: "succeeded";
116
- /** The step's own work, excluding reading and writing its artifacts. */
117
- durationMs: number;
118
- inputArtifacts: SearchRunArtifactRef[];
119
- outputArtifact: SearchRunArtifactRef;
120
- inputCount: number | null;
121
- outputCount: number | null;
122
- runRef: SearchRunRef;
123
- /** The outcome the step chose, when it chooses one. */
124
- outcome?: string;
125
- /** The arm a fork took and why. */
126
- branch?: {
127
- decision: string;
128
- reason: string;
129
- };
130
- };
131
- /** One ledger line: a step's receipt and its span since the run opened. */
132
- export type SearchRunLedgerEntry = {
133
- receipt: SearchStageReceipt;
134
- /** The nested workflows the step ran under, outermost first. */
135
- workflowPath: string[];
136
- startedAtMs: number;
137
- endedAtMs: number;
138
- };
139
- /**
140
- * Outcome receipts shared by full and card search responses. `degraded`,
141
- * `emptyOutcome` and `termCoverage` are on every response; `diagnostics` and
142
- * `explain` are explain-only.
9
+ * What ordered the page: `image` (a look-alike), `colour` (colour distance
10
+ * inside the named colour's family), `lexical` (the query's words, by text
11
+ * rank on the index), `attribute` (the category and filters alone define the
12
+ * page) or `agent` (the search agent chose the variants and their order).
143
13
  */
14
+ export type SearchMode = OpenEnum<"image" | "colour" | "lexical" | "agent" | "attribute">;
15
+ /** The explain-only receipt shared by full and card search responses. */
144
16
  export type SearchResponseExplainReceipts = {
145
- degraded?: SearchDegraded;
146
- emptyOutcome?: SearchEmptyOutcome;
147
- termCoverage?: SearchTermCoverageReceipt;
148
17
  /**
149
- * The engine's notes written for an engineer (tier timings, cache states,
150
- * index names), under `explain` only. `note` carries the reader's sentences
151
- * alone (MG-1919).
152
- */
153
- diagnostics?: string[];
154
- /**
155
- * Present under `explain` when the Search workflow answered the request:
156
- * every step receipt, in completion order. Absent when the engine answered.
18
+ * Present under `explain` only: the receipt of the plan the one search ran
19
+ * on, with the plan itself (ADR 0031). Diagnostic, like `timings`.
157
20
  */
158
21
  explain?: {
159
- ledger: readonly SearchRunLedgerEntry[];
22
+ plan: SearchPlanReceipt;
160
23
  };
161
24
  };
162
- /** One stage in the explain-only ledger of transformations that actually ran. */
163
- export type SearchQueryTransformStage = {
164
- stage: OpenEnum<"normalise" | "resolve-spec" | "resolve-paint" | "resolve-brand">;
165
- applied: true;
166
- } | {
167
- stage: OpenEnum<"normalise" | "resolve-spec" | "resolve-paint" | "resolve-brand">;
168
- applied: false;
169
- reason: string;
170
- };
171
- /** Explain-only category gate selected by query understanding. */
172
- export type SearchCategoryAnchor = {
173
- /**
174
- * A leaf type id; the group key when `source` is `"group"`; or the canonical
175
- * category noun when `source` is `"head-noun"`. Gate on `productTypeIds`.
176
- */
177
- productTypeId: string;
178
- /**
179
- * The leaf type ids a `"group"` or `"head-noun"` anchor expanded to; absent
180
- * for leaf sources.
181
- */
182
- productTypeIds?: readonly string[];
183
- displayName: string;
184
- similarity: number;
185
- margin: number | null;
186
- source: OpenEnum<"taxonomy" | "synonym" | "display-name" | "group" | "head-noun" | "vector">;
187
- /** Hard category eligibility is applied inside lexical candidate selection. */
188
- candidateSource?: OpenEnum<"lexical">;
189
- applied: boolean;
190
- };
191
- /** Explain-only brand gate selected by query understanding. */
192
- export type SearchBrandAnchor = {
193
- brandSlug: string;
194
- brandName: string;
195
- confidence: number;
196
- margin: number | null;
197
- tier: OpenEnum<"exact_id" | "exact_alias" | "lexical" | "semantic">;
198
- variantCount: number;
199
- applied: boolean;
200
- };
201
- /** A real brand candidate the engine deliberately declined to gate on. */
202
- export type SearchBrandSkip = {
203
- reason: OpenEnum<"ambiguous" | "weak-tier" | "too-broad">;
204
- candidates?: string[];
205
- };
206
- /**
207
- * EXPLAIN-only receipt of the query shape that executed. This is distinct from
208
- * the separate Search Plan compiler operation: it reports execution and never
209
- * causes it.
210
- */
211
- export type SearchExecutionPlan = {
212
- grain: OpenEnum<"variant" | "product">;
213
- /** Which 2048D Voyage projection served a text-mode request. */
214
- textLane?: OpenEnum<"image-text" | "variant-text">;
215
- weights: {
216
- portrait: number;
217
- image: number;
218
- category: number;
219
- colour: number;
220
- lexical: number;
221
- text: number;
222
- };
223
- queryTransform: SearchQueryTransformStage[];
224
- /** Ordered, explain-only request-understanding execution trace. */
225
- steps?: {
226
- key: string;
227
- status: OpenEnum<"executed" | "skipped">;
228
- reason: string;
229
- durationMs: number;
230
- }[];
231
- categoryAnchor?: SearchCategoryAnchor;
232
- brandAnchor?: SearchBrandAnchor;
233
- brandSkipped?: SearchBrandSkip;
234
- };
235
- /** Why a compound colour constraint returned a full or short page. */
236
- export type CompoundColourAbundanceReceipt = {
237
- grain: OpenEnum<"variant" | "product">;
238
- requested: number;
239
- eligible: number;
240
- candidateWindow: number;
241
- sourceExhausted: boolean;
242
- state: OpenEnum<"filled" | "corpus_scarcity" | "evidence_unavailable" | "retrieval_window_exhausted">;
243
- };
244
- /**
245
- * Eligibility proof for an explicit or taxonomy-backed colour inside a compound
246
- * text query. Counts describe the bounded candidate pool, not the whole corpus.
247
- */
248
- export type CompoundColourConstraintReceipt = {
249
- basis: OpenEnum<"explicit" | "detected-strong">;
250
- family: string;
251
- queryHexes: string[];
252
- considered: number;
253
- kept: number;
254
- dropped: number;
255
- unknown: number;
256
- adjacent: number;
257
- abundance?: CompoundColourAbundanceReceipt;
258
- };
259
- /** Why a hard form constraint returned a full or short page. */
260
- export type CompoundFormAbundanceReceipt = {
261
- grain: OpenEnum<"variant" | "product">;
262
- requested: number;
263
- eligible: number;
264
- candidateWindow: number;
265
- sourceExhausted: boolean;
266
- state: OpenEnum<"filled" | "corpus_scarcity" | "evidence_unavailable" | "retrieval_window_exhausted">;
267
- };
268
- /**
269
- * Hard form eligibility for a compound text query. Product and variant names
270
- * may retrieve a candidate but never prove that it satisfies the form.
271
- */
272
- export type CompoundFormConstraintReceipt = {
273
- instruction: OpenEnum<"subway" | "plank" | "square" | "hexagon" | "penny-round">;
274
- considered: number;
275
- kept: number;
276
- dropped: number;
277
- contradicted: number;
278
- unknown: number;
279
- abundance: CompoundFormAbundanceReceipt;
280
- };