@instruments/materialgraph 0.16.0 → 0.18.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.
@@ -32,8 +32,8 @@ export interface SearchInput {
32
32
  };
33
33
  mood?: string;
34
34
  /**
35
- * Re-order the final scored results. `relevance` (default) keeps the fused
36
- * score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
35
+ * Re-order the final scored results. `relevance` (default) keeps the selected
36
+ * mode's primary score; `light` / `dark` / `vivid` / `muted` / `colourful` are the colour
37
37
  * ramps; `name` (A→Z), `name_desc` (Z→A) and `recent` (newest first) are the
38
38
  * catalogue orders — the same three `/search/browse` walks the corpus in, so
39
39
  * one word means one order on both lanes.
@@ -100,12 +100,18 @@ export interface SearchInput {
100
100
  boost?: string;
101
101
  filter?: string[];
102
102
  };
103
+ /**
104
+ * Optional multipliers for the selected mode's one ordering signal. At most
105
+ * one is applied: image for image/image-text, lexical for lexical/browse, and
106
+ * colour for colour mode. Non-mode keys are accepted for compatibility but
107
+ * cannot activate secondary scoring.
108
+ */
103
109
  weights?: {
104
110
  image?: number;
105
111
  category?: number;
106
112
  colour?: number;
107
113
  lexical?: number;
108
- /** Text rerank multiplier applied when a seeded search also carries `text` (default 0.4). */
114
+ /** Compatibility field; seeded text does not activate secondary scoring. */
109
115
  text?: number;
110
116
  };
111
117
  filters?: SearchFilter[];
@@ -120,6 +126,7 @@ export interface SearchInput {
120
126
  /** The previous response's `cursor`, passed back verbatim. Never synthesise one. */
121
127
  cursor?: Cursor;
122
128
  explain?: boolean;
129
+ /** Public POST defaults to distinct products; send `"none"` for individual variants. */
123
130
  collapse?: OpenEnum<"none" | "product">;
124
131
  /**
125
132
  * Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools.
@@ -156,7 +163,7 @@ export interface SearchInput {
156
163
  * `"search"` (default) is the full engine. `"typeahead"` is the as-you-type
157
164
  * profile: lexical-only with prefix match on the final token, forced
158
165
  * `view: "card"`, `limit` capped at 24, and no mood/colour detection tiers,
159
- * semantic fallback, category boost, colour rerank, refinement suggestions,
166
+ * semantic fallback, secondary scoring, refinement suggestions,
160
167
  * counts, or meanings. `textModes` is ignored under typeahead.
161
168
  */
162
169
  intent?: OpenEnum<"search" | "typeahead">;
@@ -166,22 +173,26 @@ export interface SearchInput {
166
173
  * **ABSENT means the WHOLE GRAPH** — every material MaterialGraph knows,
167
174
  * orderable or not. Absence is never a narrower default, so a caller that
168
175
  * says nothing gets materials it may not be able to buy.
169
- * Three scopes gate on CHANNEL membership, which is what "can someone buy
170
- * this" means: `"materialBankNaOrderable"` (Material Bank North America,
176
+ * Three transaction-oriented scopes gate on governed CHANNEL membership:
177
+ * `"materialBankNaOrderable"` (Material Bank North America,
171
178
  * ~277,000 materials), `"materialBankEuOrderable"` (Material Bank Europe,
172
179
  * ~65,000) and `"designShopOrderable"` (DesignShop, ~34,000, with the
173
180
  * products DesignShop hides from browse excluded).
181
+ * `"materialBankNaAndCatalogue"` is a governed discovery corpus: the single
182
+ * producer-side union of Material Bank North America and Catalogue editorial
183
+ * membership. It is not a buyability claim, and a catalogue reference alone
184
+ * does not establish membership.
174
185
  *
175
186
  * `"materialBankOrderable"` is NOT one of them, despite the name. It selects
176
187
  * materials carrying a Material Bank identity — a PROVENANCE signal, not a
177
188
  * storefront — and admits about 98% of the graph. Use it to exclude the
178
189
  * crawl-sourced brands that were never in the Material Bank feed; if you mean
179
- * orderable, use one of the three above.
190
+ * orderable, use one of the channel-backed scopes above.
180
191
  *
181
- * A scope answers what is BUYABLE. It never answers what is SUITABLE: scope
182
- * on channel, then refine on sector. A trade-only material can be right for a
183
- * room and still unbuyable, and most of what a consumer can buy is
184
- * commercial-suited product.
192
+ * A scope answers membership in its named distribution or visibility
193
+ * surface; the token-specific description states whether that means
194
+ * transaction or editorial inclusion. It never answers what is SUITABLE:
195
+ * scope on channel, then refine on sector.
185
196
  *
186
197
  * The scope is applied INSIDE the query: `limit` fills with eligible rows,
187
198
  * `counts` describe the SCOPED corpus, and the response echoes the scope.
@@ -203,6 +214,8 @@ export interface SearchGetParams {
203
214
  colours?: string;
204
215
  logic?: OpenEnum<"all" | "any">;
205
216
  mood?: string;
217
+ /** Governed corpus applied inside retrieval; absent means the whole graph. */
218
+ corpusScope?: CorpusScope;
206
219
  sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful" | "name" | "name_desc" | "recent">;
207
220
  tol?: OpenEnum<"tight" | "normal" | "loose">;
208
221
  /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
@@ -1,17 +1,55 @@
1
1
  /**
2
2
  * @module sdk/search-response-receipt-types
3
- * @description Inspectable routing, rerank, and compound-constraint receipts
4
- * emitted by ordinary Search responses.
3
+ * @description Inspectable routing, compatibility, and compound-constraint
4
+ * receipts emitted by ordinary Search responses.
5
5
  */
6
6
  import type { OpenEnum } from "./types.js";
7
- /** The single candidate source a query resolved to. */
7
+ /** The primary ranking source a query resolved to and reports as its mode. */
8
8
  export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical" | "entity-browse">;
9
- /** Rerank-term report (absent in colour mode — there colour IS the source). */
9
+ /**
10
+ * Compatibility receipt for non-mode colour handling. Current one-signal
11
+ * ordering reports it skipped at weight 0; colour may still be diagnostic or a
12
+ * hard displayed-image constraint, but does not blend into score.
13
+ */
10
14
  export interface SearchRerankReport {
11
15
  colour?: OpenEnum<"ok" | "failed" | "skipped">;
12
16
  colourWeight?: number;
13
17
  colourLabel?: string;
14
18
  }
19
+ /** Stage counts retained when an explain-enabled search returns no results. */
20
+ export interface SearchEmptyOutcomeCounts {
21
+ retrieved: number;
22
+ afterPrune: number;
23
+ final: number;
24
+ }
25
+ /**
26
+ * Explain-only evidence for why a completed search returned no results.
27
+ * `proven-corpus-empty` is exhaustive; every other empty remains explicitly
28
+ * unproven rather than being presented as catalogue absence.
29
+ */
30
+ export type SearchEmptyOutcome = {
31
+ kind: "proven-corpus-empty";
32
+ basis: OpenEnum<"spec-allowlist" | "brand-scope">;
33
+ exhaustive: true;
34
+ sourceEmpty?: boolean | null;
35
+ counts?: SearchEmptyOutcomeCounts;
36
+ } | {
37
+ kind: "unproven-empty";
38
+ reason: OpenEnum<"allowlist-truncated" | "retrieval-window-not-exhaustive" | "source-empty" | "post-retrieval-rejected" | "pagination-empty" | "no-authoritative-empty-proof">;
39
+ sourceEmpty?: boolean | null;
40
+ counts?: SearchEmptyOutcomeCounts;
41
+ };
42
+ /** Explain-only coverage of substantive query terms in the lexical projection. */
43
+ export interface SearchTermCoverageReceipt {
44
+ status: OpenEnum<"covered" | "uncovered" | "unavailable" | "skipped">;
45
+ terms: string[];
46
+ note?: string;
47
+ }
48
+ /** Explain-only outcome receipts shared by full and card search responses. */
49
+ export interface SearchResponseExplainReceipts {
50
+ emptyOutcome?: SearchEmptyOutcome;
51
+ termCoverage?: SearchTermCoverageReceipt;
52
+ }
15
53
  /** One stage in the explain-only ledger of transformations that actually ran. */
16
54
  export type SearchQueryTransformStage = {
17
55
  stage: OpenEnum<"normalise" | "resolve-spec" | "resolve-paint" | "resolve-brand">;
@@ -23,11 +61,22 @@ export type SearchQueryTransformStage = {
23
61
  };
24
62
  /** Explain-only category gate selected by query understanding. */
25
63
  export interface SearchCategoryAnchor {
64
+ /**
65
+ * A leaf type id; the group key when `source` is `"group"`; or the canonical
66
+ * category noun when `source` is `"head-noun"`. Gate on `productTypeIds`.
67
+ */
26
68
  productTypeId: string;
69
+ /**
70
+ * The leaf type ids a `"group"` or `"head-noun"` anchor expanded to; absent
71
+ * for leaf sources.
72
+ */
73
+ productTypeIds?: readonly string[];
27
74
  displayName: string;
28
75
  similarity: number;
29
76
  margin: number | null;
30
- source: OpenEnum<"synonym" | "display-name" | "vector">;
77
+ source: OpenEnum<"taxonomy" | "synonym" | "display-name" | "group" | "head-noun" | "vector">;
78
+ /** Hard category eligibility is applied inside lexical candidate selection. */
79
+ candidateSource?: OpenEnum<"lexical">;
31
80
  applied: boolean;
32
81
  }
33
82
  /** Explain-only brand gate selected by query understanding. */
@@ -52,7 +101,10 @@ export interface SearchBrandSkip {
52
101
  */
53
102
  export interface SearchExecutionPlan {
54
103
  grain: OpenEnum<"variant" | "product">;
104
+ /** Which 2048D Voyage projection served a text-mode request. */
105
+ textLane?: OpenEnum<"image-text" | "variant-text">;
55
106
  weights: {
107
+ portrait: number;
56
108
  image: number;
57
109
  category: number;
58
110
  colour: number;
@@ -60,6 +112,13 @@ export interface SearchExecutionPlan {
60
112
  text: number;
61
113
  };
62
114
  queryTransform: SearchQueryTransformStage[];
115
+ /** Ordered, explain-only request-understanding execution trace. */
116
+ steps?: Array<{
117
+ key: string;
118
+ status: OpenEnum<"executed" | "skipped">;
119
+ reason: string;
120
+ durationMs: number;
121
+ }>;
63
122
  categoryAnchor?: SearchCategoryAnchor;
64
123
  brandAnchor?: SearchBrandAnchor;
65
124
  brandSkipped?: SearchBrandSkip;
@@ -78,7 +137,7 @@ export interface CompoundColourAbundanceReceipt {
78
137
  * text query. Counts describe the bounded candidate pool, not the whole corpus.
79
138
  */
80
139
  export interface CompoundColourConstraintReceipt {
81
- provenance: OpenEnum<"explicit" | "detected-strong">;
140
+ basis: OpenEnum<"explicit" | "detected-strong">;
82
141
  family: string;
83
142
  queryHexes: string[];
84
143
  considered: number;
@@ -88,3 +147,25 @@ export interface CompoundColourConstraintReceipt {
88
147
  adjacent: number;
89
148
  abundance?: CompoundColourAbundanceReceipt;
90
149
  }
150
+ /** Why a hard form constraint returned a full or short page. */
151
+ export interface CompoundFormAbundanceReceipt {
152
+ grain: OpenEnum<"variant" | "product">;
153
+ requested: number;
154
+ eligible: number;
155
+ candidateWindow: number;
156
+ sourceExhausted: boolean;
157
+ state: OpenEnum<"filled" | "corpus_scarcity" | "evidence_unavailable" | "retrieval_window_exhausted">;
158
+ }
159
+ /**
160
+ * Hard form eligibility for a compound text query. Product and variant names
161
+ * may retrieve a candidate but never prove that it satisfies the form.
162
+ */
163
+ export interface CompoundFormConstraintReceipt {
164
+ instruction: OpenEnum<"subway" | "plank" | "square" | "hexagon" | "penny-round">;
165
+ considered: number;
166
+ kept: number;
167
+ dropped: number;
168
+ contradicted: number;
169
+ unknown: number;
170
+ abundance: CompoundFormAbundanceReceipt;
171
+ }
@@ -49,6 +49,23 @@ function checkPaletteSwatch(value, what, fail) {
49
49
  if (record.name !== undefined)
50
50
  requireNullableString(record.name, `${what}.name`, fail);
51
51
  }
52
+ /**
53
+ * `variants[]` — one entry of a product-collapsed result's ranked variant list
54
+ * (`collapsedVariantSchema`). A SLIM projection, not a nested result item: the
55
+ * checks here are the whole shape.
56
+ */
57
+ function checkCollapsedVariant(value, what, fail) {
58
+ const record = requireRecord(value, what, fail);
59
+ requireString(record.variantId, `${what}.variantId`, fail);
60
+ requireNullableString(record.variantName, `${what}.variantName`, fail);
61
+ requireNullableString(record.color, `${what}.color`, fail);
62
+ // `.optional()`, NOT nullable: the emit path omits the key when no palette is
63
+ // on record rather than sending null.
64
+ requireOptionalString(record.hex, `${what}.hex`, fail);
65
+ requireNullableString(record.imageUrl, `${what}.imageUrl`, fail);
66
+ requireOptionalString(record.materialBankId, `${what}.materialBankId`, fail);
67
+ requireNumber(record.score, `${what}.score`, fail);
68
+ }
52
69
  /** `colourMeta` — three independently optional strings. */
53
70
  function checkColourMeta(value, what, fail) {
54
71
  const record = requireRecord(value, what, fail);
@@ -56,9 +73,12 @@ function checkColourMeta(value, what, fail) {
56
73
  requireOptionalString(record.dominantFamily, `${what}.dominantFamily`, fail);
57
74
  requireOptionalString(record.dominantHex, `${what}.dominantHex`, fail);
58
75
  }
59
- /** `searchResultSignalsSchema` — five nullable numbers, all required keys. */
76
+ /** `searchResultSignalsSchema` — five required scores plus additive portrait. */
60
77
  function checkSignals(value, what, fail) {
61
78
  const record = requireRecord(value, what, fail);
79
+ if (record.portrait !== undefined) {
80
+ requireNullableNumber(record.portrait, `${what}.portrait`, fail);
81
+ }
62
82
  for (const signal of ["image", "category", "colour", "lexical", "text"]) {
63
83
  requireNullableNumber(record[signal], `${what}.${signal}`, fail);
64
84
  }
@@ -192,6 +212,10 @@ export function checkSearchResultItem(value, what, fail) {
192
212
  if (record.siblingCount !== undefined) {
193
213
  requireIntegerAtLeast(record.siblingCount, `${what}.siblingCount`, fail, 1);
194
214
  }
215
+ if (record.variants !== undefined) {
216
+ const variants = requireArray(record.variants, `${what}.variants`, fail);
217
+ variants.forEach((variant, index) => checkCollapsedVariant(variant, `${what}.variants[${index}]`, fail));
218
+ }
195
219
  if (record.visualMeanings !== undefined) {
196
220
  const meanings = requireArray(record.visualMeanings, `${what}.visualMeanings`, fail);
197
221
  meanings.forEach((meaning, index) => checkVisualMeaning(meaning, `${what}.visualMeanings[${index}]`, fail));
@@ -4,14 +4,22 @@ export interface SearchWindowReceipt {
4
4
  profile: "interactive-r1" | (string & {});
5
5
  /** Candidate depth used by the final retrieval. */
6
6
  candidateTier: number;
7
- /** Every candidate retrieval, including required fallbacks and optional recovery. */
7
+ /** Candidate retrieval count; `searchMaterials` reports exactly one selected retrieval. */
8
8
  retrievalRounds: number;
9
9
  /** Candidate-retrieval wall time only, in milliseconds. */
10
10
  retrievalMs: number;
11
11
  /** True when the serving source proved there were no more candidates. */
12
12
  exhaustive: boolean;
13
- /** True when product-grain scarcity caused one jump to the deep ceiling. */
14
- deepened: boolean;
15
- /** True when a still-short page exhausted its bounded recovery allowance. */
16
- budgetExhausted: boolean;
13
+ /**
14
+ * Candidate attrition through the selected retrieval source. Absent when the
15
+ * response path did not perform ordinary retrieval (for example typeahead).
16
+ */
17
+ candidateCounts?: {
18
+ /** Provider-returned matches before bounded selection; null for SQL/exact arms. */
19
+ rawProviderMatches: number | null;
20
+ /** Matches selected from that provider response; null for SQL/exact arms. */
21
+ selectedMatches: number | null;
22
+ /** Canonical candidates returned by the selected retrieval arm. */
23
+ hydratedCanonicalCandidates: number;
24
+ };
17
25
  }
package/spec-types.d.ts CHANGED
@@ -70,10 +70,16 @@ export interface SpecTermFilter {
70
70
  * {@link CERTIFICATION_PROGRAMME_ATTRIBUTE_ID} (a programme is matched by name,
71
71
  * never by bare presence).
72
72
  */
73
- export interface SpecPresenceFilter {
73
+ export type SpecPresenceFilter = {
74
74
  attributeId: string;
75
75
  hasValue: true;
76
76
  }
77
+ /** Date-sensitive presence is currently executable only for EPD expiration. */
78
+ | {
79
+ attributeId: "epd_expiration_date";
80
+ hasValue: true;
81
+ asOf: string;
82
+ };
77
83
  /**
78
84
  * A certification-programme criterion — a {@link SpecTermFilter} pinned to the
79
85
  * virtual {@link CERTIFICATION_PROGRAMME_ATTRIBUTE_ID}. Structurally assignable
@@ -12,6 +12,7 @@
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";
15
16
  /** Which kind of entity a suggestion is — and therefore what `refId` means. */
16
17
  export type SuggestKind = "product" | "brand" | "category";
17
18
  /** How the query met the suggestion's label. Strongest first. */
@@ -44,6 +45,8 @@ export interface SuggestResponse {
44
45
  query: string;
45
46
  /** Ranked best-match-first. Empty when nothing came close — a miss, not an error. */
46
47
  suggestions: Suggestion[];
48
+ /** Present only when a narrowing governed corpus was applied. */
49
+ corpusScope?: CorpusScopeReceipt;
47
50
  }
48
51
  /** Options for {@link MaterialGraphClient.suggest}. */
49
52
  export interface SuggestOptions {
@@ -51,6 +54,8 @@ export interface SuggestOptions {
51
54
  limit?: number;
52
55
  /** Restrict to these kinds. Omit for all of them. */
53
56
  kind?: SuggestKind | SuggestKind[];
57
+ /** Governed corpus membership; absent means the whole graph. */
58
+ corpusScope?: CorpusScope;
54
59
  }
55
60
  /**
56
61
  * Shape the query string for a suggest call.
@@ -64,4 +69,5 @@ export declare function suggestQuery(q: string, options?: SuggestOptions): {
64
69
  q: string;
65
70
  limit?: number;
66
71
  kind?: string;
72
+ corpusScope?: CorpusScope;
67
73
  };
package/suggest-types.js CHANGED
@@ -24,5 +24,5 @@ export function suggestQuery(q, options) {
24
24
  const kind = Array.isArray(options?.kind)
25
25
  ? options.kind.join(",")
26
26
  : options?.kind;
27
- return { q, limit: options?.limit, kind };
27
+ return { q, limit: options?.limit, kind, corpusScope: options?.corpusScope };
28
28
  }
package/types.d.ts CHANGED
@@ -30,8 +30,9 @@ import type { CorpusScopeReceipt } from "./corpus-scope-types.js";
30
30
  import type { Cursor, ProductId, VariantId } from "./identifiers.js";
31
31
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
32
32
  import type { QueryUnderstanding } from "./query-understanding-types.js";
33
+ import type { CollapsedVariant, SearchAnatomyMember } from "./search-collapse-types.js";
33
34
  import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
34
- import type { CompoundColourConstraintReceipt, SearchExecutionPlan, SearchMode, SearchRerankReport } from "./search-response-receipt-types.js";
35
+ import type { CompoundColourConstraintReceipt, CompoundFormConstraintReceipt, SearchExecutionPlan, SearchMode, SearchRerankReport, SearchResponseExplainReceipts } from "./search-response-receipt-types.js";
35
36
  import type { SearchWindowReceipt } from "./search-window-types.js";
36
37
  export type * from "./search-response-receipt-types.js";
37
38
  export type OpenEnum<T extends string> = T | (string & {});
@@ -89,11 +90,13 @@ export type ImageSource = OpenEnum<"r2" | "origin">;
89
90
  export type { DimensionEntry, MaterialAvailability, MaterialDetail, MaterialFacet, MaterialSuccessor, MaterialTile, } from "./material-types.js";
90
91
  /** Per-signal scores, present on a result when `explain` was requested. */
91
92
  export interface SearchResultSignals {
93
+ /** Voyage similarity in the dedicated variant text-portrait space. */
94
+ portrait?: number | null;
92
95
  image: number | null;
93
96
  category: number | null;
94
97
  colour: number | null;
95
98
  lexical: number | null;
96
- /** Text→image RERANK cosine — non-null only for a seeded search carrying `text`. */
99
+ /** Compatibility slot for retired seeded secondary scoring; currently null. */
97
100
  text: number | null;
98
101
  }
99
102
  /** An accepted/measured visual meaning — descriptive evidence, never a ranking cause. */
@@ -107,15 +110,6 @@ export interface VisualMeaning {
107
110
  version?: string;
108
111
  appliedAt?: string;
109
112
  }
110
- /** One member of a collapsed result's {@link SearchResultItem.anatomy}. */
111
- export interface SearchAnatomyMember {
112
- /** Opaque — round-trip verbatim. */
113
- variantId: VariantId;
114
- variantName: string | null;
115
- color: string | null;
116
- score: number;
117
- signals: SearchResultSignals;
118
- }
119
113
  /**
120
114
  * One material in a search / match result list.
121
115
  *
@@ -198,7 +192,7 @@ export interface SearchResultItem {
198
192
  siblings?: SearchAnatomyMember[];
199
193
  siblingsTruncated?: boolean;
200
194
  };
201
- /** Fused score — range [0, sum of active weights]; see `maxScore`. */
195
+ /** Selected mode's weighted primary score; see `maxScore`. */
202
196
  score: number;
203
197
  /**
204
198
  * Match only. How close this material is to the query, as an ABSOLUTE
@@ -248,7 +242,10 @@ export interface SearchResultItem {
248
242
  * `includeBelowFloor`.
249
243
  */
250
244
  admittedOnColour?: boolean;
245
+ /** Variants HIDDEN behind this card by `collapse: "product"` — the COMPLETE tally, even when {@link variants} is capped. */
251
246
  siblingCount?: number;
247
+ /** The product's matching variants, MOST RELEVANT FIRST — present exactly under `collapse: "product"`, capped, and led by the group's RANKING representative, which is NOT always this item. {@link CollapsedVariant} states the full contract. */
248
+ variants?: CollapsedVariant[];
252
249
  visualMeanings?: VisualMeaning[];
253
250
  /**
254
251
  * The shared report card for this hit against the request's `filters` (required
@@ -403,20 +400,19 @@ export interface SearchRefinementReceipt {
403
400
  * you asked for it. Follow `cursor` for the rest. "I found 24 materials" is
404
401
  * wrong whenever `limit` was 24.
405
402
  */
406
- export interface SearchResponse {
403
+ export interface SearchResponse extends SearchResponseExplainReceipts {
407
404
  view: OpenEnum<"full" | "card">;
408
405
  /** THIS PAGE. Its length is a page size, never a total. */
409
406
  results: SearchResultItem[];
410
407
  suggestions: SearchSuggestion[];
411
408
  mode: SearchMode;
412
409
  maxScore: number;
413
- /** Opaque keyset for the next page. Absent = end of the walk, not an error. */
414
410
  cursor?: Cursor;
415
- /** Boundedness and retrieval-cost receipt on current Search responses. */
416
411
  window?: SearchWindowReceipt;
417
412
  rerank?: SearchRerankReport;
418
- /** Hard compound-colour eligibility and abundance receipt, when applied. */
413
+ /** Hard compound eligibility and abundance receipts, when applied. */
419
414
  colourConstraint?: CompoundColourConstraintReceipt;
415
+ formConstraint?: CompoundFormConstraintReceipt;
420
416
  /** Explain-only receipt of the query shape and transform ledger that ran. */
421
417
  plan?: SearchExecutionPlan;
422
418
  counts?: SearchCounts;
@@ -443,8 +439,9 @@ export interface SearchResponse {
443
439
  /** A detected mood term overridden by a stronger colour reading. */
444
440
  supersededMoodId?: string;
445
441
  /**
446
- * Present when the query had TWO defensible readings. Says which one ran and
447
- * offers the other as a complete executable request.
442
+ * Present when the query had multiple defensible readings. Says which one
443
+ * ran, or leaves every reading non-default when the engine chose none. Every
444
+ * reading carries a complete executable replacement request.
448
445
  */
449
446
  disambiguation?: SearchDisambiguation;
450
447
  /** Structured receipt of the standard query-understanding decisions. */