@instruments/materialgraph 0.6.0 → 0.7.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.
package/types.d.ts CHANGED
@@ -11,8 +11,22 @@
11
11
  * that mirrors a server enum is therefore widened with `(string & {})` so a new
12
12
  * server value still type-checks, and objects that may grow fields are read
13
13
  * structurally (unknown keys pass through untouched — the client never strips).
14
+ *
15
+ * READING CONTRACT — the three things callers get wrong, stated once here and
16
+ * repeated on the types where each one bites:
17
+ * 1. **Ids are opaque.** Round-trip them verbatim; never parse or construct
18
+ * one. See `./identifiers.ts` for which strings are opaque tokens and which
19
+ * are canonical vocabulary you discover.
20
+ * 2. **Absent is not false.** An optional field that is missing means UNKNOWN,
21
+ * not "known to be absent in reality" — most dangerously for booleans.
22
+ * Values carry `confidence` and `sourceBasis`; report them as claims.
23
+ * 3. **A page is not a corpus.** `results.length` is this page.
24
+ * {@link CountedTotal} — with the `basis` it states — is the claim about the
25
+ * match set. Follow the cursor for the rest, and never report a page size as
26
+ * a total.
14
27
  */
15
28
  import type { BySpecVerdict } from "./by-spec-types.js";
29
+ import type { Cursor, ProductId, VariantId } from "./identifiers.js";
16
30
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
17
31
  import type { SearchFilter, SearchSuggestion } from "./search-refinement-types.js";
18
32
  /** A widened string-literal union: known members autocomplete, unknown values still type-check. */
@@ -79,13 +93,22 @@ export interface VisualMeaning {
79
93
  version?: string;
80
94
  appliedAt?: string;
81
95
  }
82
- /** One material in a search / match result list. */
96
+ /**
97
+ * One material in a search / match result list.
98
+ *
99
+ * ABSENT IS NOT FALSE. The optional fields here (`colourMeta`, `signals`,
100
+ * `retired`, `siblingCount`, `visualMeanings`, `verdict`) are absent when the
101
+ * request did not ask for them or the graph holds no answer — never because the
102
+ * answer is "no". A missing `retired` is not "still current"; a missing
103
+ * `visualMeanings` is not "this material has no visual meaning".
104
+ */
83
105
  export interface SearchResultItem {
84
- variantId: string;
106
+ /** Opaque — round-trip verbatim into `material()`, `pairings()`, `similarTo`. */
107
+ variantId: VariantId;
85
108
  brand: string;
86
109
  product: string;
87
110
  brandSlug: string | null;
88
- productId: string | null;
111
+ productId: ProductId | null;
89
112
  color: string | null;
90
113
  variantName: string | null;
91
114
  imageUrl: string | null;
@@ -112,10 +135,18 @@ export interface SearchResultItem {
112
135
  */
113
136
  verdict?: BySpecVerdict;
114
137
  }
115
- /** The lean `view:"card"` result item — identity + name + thumbnail + score. */
138
+ /**
139
+ * The lean `view:"card"` result item — identity + name + thumbnail + score.
140
+ *
141
+ * A PROJECTION, not a smaller answer: a field the card omits (palette, verdict,
142
+ * signals, availability) is unread here, never known-absent. Re-read the same
143
+ * `variantId` at `view: "full"` or via `material()` before claiming anything the
144
+ * card does not carry.
145
+ */
116
146
  export interface SearchCardItem {
117
- variantId: string;
118
- productId: string | null;
147
+ /** Opaque — round-trip verbatim. */
148
+ variantId: VariantId;
149
+ productId: ProductId | null;
119
150
  brandSlug: string | null;
120
151
  brand: string;
121
152
  /** Product name (the full item's `product`). */
@@ -125,15 +156,26 @@ export interface SearchCardItem {
125
156
  score: number;
126
157
  }
127
158
  /**
128
- * A feed total with its honesty basis. `exact` — the true count (≤ the cap of
129
- * 1,000, or a facet-MV read). `lowerBound` — a floor (cap hit, ANN-pool bound,
130
- * or a degraded count). Never a silent approximation.
159
+ * A feed total with its honesty basis — THE CLAIM ABOUT THE MATCH SET, which is
160
+ * a different number from `results.length` (this page).
161
+ *
162
+ * `exact` — the true count (≤ the cap of 1,000, or a facet-MV read). Say "473
163
+ * materials match".
164
+ * `lowerBound` — a floor (cap hit, ANN-pool bound, or a degraded count). Say
165
+ * "at least 1,000", never "1,000". Never a silent approximation.
166
+ *
167
+ * A response with no `counts` at all states no total: `count: true` is opt-in,
168
+ * and the absence of a total is not a total of zero.
131
169
  */
132
170
  export interface CountedTotal {
133
171
  value: number;
134
172
  basis: OpenEnum<"exact" | "lowerBound">;
135
173
  }
136
- /** Feed counts — present under `count:true` or `collapse:"product"`. */
174
+ /**
175
+ * Feed counts — present ONLY under `count:true` or `collapse:"product"`. This is
176
+ * the corpus-level claim; `results.length` is the page. Never report one as the
177
+ * other.
178
+ */
137
179
  export interface SearchCounts {
138
180
  variants: CountedTotal;
139
181
  products?: CountedTotal;
@@ -152,14 +194,21 @@ export interface SearchRerankReport {
152
194
  * {@link SearchCardItem}s. Every other field is shared. `counts` is present
153
195
  * under `count:true` or `collapse:"product"` (replacing the former
154
196
  * `variantCount`/`productCount`).
197
+ *
198
+ * A PAGE, NOT A CORPUS. `results.length` is what you were handed;
199
+ * {@link SearchCounts} is the only statement about the match set, and only when
200
+ * you asked for it. Follow `cursor` for the rest. "I found 24 materials" is
201
+ * wrong whenever `limit` was 24.
155
202
  */
156
203
  export interface SearchResponse {
157
204
  view: OpenEnum<"full" | "card">;
205
+ /** THIS PAGE. Its length is a page size, never a total. */
158
206
  results: SearchResultItem[];
159
207
  suggestions: SearchSuggestion[];
160
208
  mode: SearchMode;
161
209
  maxScore: number;
162
- cursor?: string;
210
+ /** Opaque keyset for the next page. Absent = end of the walk, not an error. */
211
+ cursor?: Cursor;
163
212
  rerank?: SearchRerankReport;
164
213
  counts?: SearchCounts;
165
214
  note?: string;
@@ -180,8 +229,17 @@ export interface MatchCategoryInference {
180
229
  displayName: string;
181
230
  boost: number;
182
231
  }
183
- /** The Material Match response (non-streaming form). */
232
+ /**
233
+ * The Material Match response (non-streaming form).
234
+ *
235
+ * `poolSize` is the candidate pool the floor was applied to, NOT a corpus total
236
+ * and NOT "how many materials look like this". An empty `results` with
237
+ * `droppedBelowFloor > 0` means "candidates existed and none was good enough" —
238
+ * a different answer from `poolSize: 0`, which means the image found nothing to
239
+ * search against at all.
240
+ */
184
241
  export interface MatchResponse {
242
+ /** THIS answer, capped by `limit`. Not a total. */
185
243
  results: SearchResultItem[];
186
244
  inferredCategories: MatchCategoryInference[];
187
245
  qualityFloor: number;
@@ -195,9 +253,17 @@ export interface MatchCategory {
195
253
  group: string | null;
196
254
  type: string | null;
197
255
  }
198
- /** One colour-palette match. */
256
+ /**
257
+ * One colour-palette match.
258
+ *
259
+ * `hex`, `distance` and `closeness` are OKLab geometry over colour EXTRACTED
260
+ * FROM PRODUCT IMAGERY, not declared by the manufacturer. Report it as visual
261
+ * likeness ("reads close to this colour"), never as a stated colour
262
+ * specification.
263
+ */
199
264
  export interface ByColourMatch {
200
- variantId: string;
265
+ /** Opaque — round-trip verbatim. */
266
+ variantId: VariantId;
201
267
  brand: string;
202
268
  product: string;
203
269
  color: string | null;
@@ -216,16 +282,18 @@ export interface ByColourResponse {
216
282
  categories?: string[];
217
283
  oklab?: OklabTriple;
218
284
  };
285
+ /** `matches.length` — THIS PAGE. Not a corpus total; this surface states none. */
219
286
  count: number;
220
287
  matches: ByColourMatch[];
221
288
  }
222
289
  /** One item from the pure colour-scalar browse. */
223
290
  export interface BrowseResultItem {
224
- variantId: string;
291
+ /** Opaque — round-trip verbatim. */
292
+ variantId: VariantId;
225
293
  brand: string;
226
294
  product: string;
227
295
  brandSlug: string | null;
228
- productId: string | null;
296
+ productId: ProductId | null;
229
297
  color: string | null;
230
298
  variantName: string | null;
231
299
  imageUrl: string | null;
@@ -236,10 +304,17 @@ export interface BrowseResultItem {
236
304
  };
237
305
  retired?: boolean;
238
306
  }
239
- /** A single page of the colour browse. `cursor` is the keyset for the next page. */
307
+ /**
308
+ * A single page of the colour browse — the SDK's one genuinely cursor-paginated
309
+ * surface. `results.length` is this page and the browse states no total at all,
310
+ * so a corpus claim can only come from walking every page
311
+ * (`client.browse.pages(...)` does the loop).
312
+ */
240
313
  export interface BrowseResponse {
314
+ /** THIS PAGE. */
241
315
  results: BrowseResultItem[];
242
- cursor?: string;
316
+ /** Opaque keyset for the next page. Absent = the walk is finished. */
317
+ cursor?: Cursor;
243
318
  }
244
319
  export type ColourDictionaryId = OpenEnum<"colour_family" | "colour_mood" | "colour_undertone">;
245
320
  export interface ColourFacetTerm {
@@ -285,17 +360,34 @@ export interface ProjectTypeShare {
285
360
  projectType: string;
286
361
  projectCount: number;
287
362
  }
288
- /** A pairing item backed by real co-specification (order) evidence. */
363
+ /**
364
+ * A pairing item from the behavioural arm — real order evidence, or board
365
+ * curation when this seed has no order edges.
366
+ *
367
+ * WHAT YOU MAY CLAIM, per `source`:
368
+ * - `cospec` — real ORDER co-occurrence. The two materials were ordered
369
+ * together on real projects. "Specified together" is honest here.
370
+ * - `considered` — BOARD ACTIVITY only (moodboard curation). Somebody put them
371
+ * on a board together; nobody is known to have bought them together. Say
372
+ * "often boarded together" / "considered alongside" and NEVER "specified
373
+ * together". The server falls back to this arm seed-by-seed when the seed
374
+ * carries no `sampled` edges, so a single response can be entirely
375
+ * `considered`.
376
+ *
377
+ * See {@link ColourPairing} for the third, non-evidential arm.
378
+ */
289
379
  export interface CospecPairing {
380
+ /** The evidence tier. `cospec` = orders; `considered` = boards. Read it before phrasing. */
290
381
  source: "cospec" | "considered";
291
- variantId: string;
382
+ /** Opaque — round-trip verbatim. */
383
+ variantId: VariantId;
292
384
  brand: string;
293
385
  product: string;
294
386
  color: string | null;
295
387
  variantName: string | null;
296
388
  imageUrl: string | null;
297
389
  brandSlug: string;
298
- productId: string;
390
+ productId: ProductId;
299
391
  category: string | null;
300
392
  projectCount: number;
301
393
  customerCount: number;
@@ -303,18 +395,63 @@ export interface CospecPairing {
303
395
  liftLabel: string;
304
396
  demand: CompanionDemand | null;
305
397
  topProjectTypes: ProjectTypeShare[];
398
+ /** Graph-owned colour ranking facts, present only for an explicitly ordered rail. */
399
+ ranking?: PairingRanking;
306
400
  }
307
- /** A pairing item topped up from colour palette-similarity (merely looks adjacent). */
401
+ /**
402
+ * A pairing item topped up from colour palette-similarity — a LOOK-ALIKE, not
403
+ * evidence. Nobody ordered these together and nobody boarded them together; the
404
+ * palettes sit near each other in OKLab. Say "shares a palette with" and never
405
+ * "specified together" or "often paired with".
406
+ */
308
407
  export interface ColourPairing extends MaterialTile {
309
408
  source: "colour";
310
409
  score: number;
410
+ /** Graph-owned colour ranking facts, present only for an explicitly ordered rail. */
411
+ ranking?: PairingRanking;
412
+ }
413
+ export type PairingOrderMode = "relevance" | "lightness_asc" | "lightness_desc";
414
+ export type PairingCorpus = "all" | "material_bank";
415
+ export interface PairingRanking {
416
+ anchorColourSimilarity: number | null;
417
+ lightness: number | null;
418
+ anchorColourSimilarityMeasurable: boolean;
419
+ lightnessMeasurable: boolean;
420
+ basis: OpenEnum<"dominant_colour_oklab">;
421
+ }
422
+ export interface PairingOrdering {
423
+ requested: OpenEnum<PairingOrderMode>;
424
+ applied: OpenEnum<PairingOrderMode>;
425
+ direction: OpenEnum<"ascending" | "descending">;
426
+ anchorColourBasis: OpenEnum<"one_minus_oklab_euclidean_distance">;
427
+ colourSpace: OpenEnum<"oklab">;
428
+ lightnessRange: {
429
+ min: 0;
430
+ max: 1;
431
+ };
432
+ missingValues: OpenEnum<"last">;
433
+ tieBreak: OpenEnum<"variantId_asc">;
311
434
  }
435
+ /**
436
+ * One pairing, discriminated by `source` into three tiers of very different
437
+ * strength: `cospec` (real orders) > `considered` (board activity) > `colour`
438
+ * (palette look-alike). Branch on `source` before you write a sentence about it.
439
+ */
312
440
  export type PairingItem = CospecPairing | ColourPairing;
313
441
  export interface PairingsResponse {
314
442
  seed: MaterialTile;
443
+ /** A rail, not a ranked corpus — mixed evidence tiers, capped by `limit`. */
315
444
  items: PairingItem[];
316
- /** How many items came from real order evidence (the cospec arm). */
445
+ /**
446
+ * How many `items` came from real ORDER evidence (`source: "cospec"`). Zero
447
+ * with a full rail means every item is board activity or colour look-alike —
448
+ * check it before presenting the rail as demand evidence.
449
+ */
317
450
  cospecCount: number;
451
+ /** Present only when `order` was requested. Evidence tiers remain independent. */
452
+ ordering?: PairingOrdering;
453
+ /** Opaque continuation bound to the seed, filters, corpus, and order. */
454
+ nextCursor?: Cursor;
318
455
  }
319
456
  export interface MaterialResponse {
320
457
  material: MaterialDetail;
@@ -324,15 +461,23 @@ export interface MaterialResponse {
324
461
  export interface SiblingsResponse {
325
462
  siblings: MaterialTile[];
326
463
  }
327
- /** Batch read of tile projections. */
464
+ /**
465
+ * Batch read of tile projections. `materials` is NOT positional — resolve by
466
+ * `variantId`, and read `missingIds` rather than inferring absence from length.
467
+ */
328
468
  export interface MaterialsBatchTileResponse {
329
469
  materials: MaterialTile[];
330
- missingIds: string[];
470
+ /** Requested ids that resolved to nothing. Echoed verbatim, opaque. */
471
+ missingIds: VariantId[];
331
472
  }
332
- /** Batch read of detail projections. */
473
+ /**
474
+ * Batch read of detail projections. `materials` is NOT positional — resolve by
475
+ * `variantId`, and read `missingIds` rather than inferring absence from length.
476
+ */
333
477
  export interface MaterialsBatchDetailResponse {
334
478
  materials: MaterialDetail[];
335
- missingIds: string[];
479
+ /** Requested ids that resolved to nothing. Echoed verbatim, opaque. */
480
+ missingIds: VariantId[];
336
481
  }
337
482
  export interface ResolveColourResponse {
338
483
  match: {
@@ -414,7 +559,7 @@ export interface SearchInput {
414
559
  * seed variant, so it is the only form `verdicts` can diff a candidate against.
415
560
  */
416
561
  similarTo?: {
417
- variantId: string;
562
+ variantId: VariantId;
418
563
  } | {
419
564
  vector: string;
420
565
  };
@@ -438,11 +583,16 @@ export interface SearchInput {
438
583
  * demotes a failed/missing preferred hit WITHIN its page only.
439
584
  */
440
585
  preferences?: SearchFilter[];
586
+ /** Page size — the size of ONE page, never a bound on the match set. */
441
587
  limit?: number;
442
- cursor?: string;
588
+ /** The previous response's `cursor`, passed back verbatim. Never synthesise one. */
589
+ cursor?: Cursor;
443
590
  explain?: boolean;
444
591
  collapse?: OpenEnum<"none" | "product">;
445
- /** Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools. */
592
+ /**
593
+ * Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools.
594
+ * Without it the response states NO total — and a page size is not one.
595
+ */
446
596
  count?: boolean;
447
597
  /** Only return materials with at least one catalog image. Default false (full catalog). */
448
598
  hasImages?: boolean;
@@ -464,7 +614,8 @@ export interface SearchGetParams {
464
614
  mood?: string;
465
615
  sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
466
616
  tol?: OpenEnum<"tight" | "normal" | "loose">;
467
- variantId?: string;
617
+ /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
618
+ variantId?: VariantId;
468
619
  limit?: number;
469
620
  /** "0" or "1" — include per-signal `signals` on each result. */
470
621
  explain?: OpenEnum<"0" | "1">;
@@ -492,7 +643,12 @@ export interface ByColourParams {
492
643
  /** Params for the pure colour `search/browse` surface. */
493
644
  export interface BrowseParams {
494
645
  sort: OpenEnum<"light" | "dark" | "vivid" | "muted" | "colourful">;
495
- cursor?: string;
646
+ /**
647
+ * The previous page's `cursor`, verbatim. Opaque keyset state — not an offset.
648
+ * Changing `sort`, `limit` or `types` invalidates it.
649
+ */
650
+ cursor?: Cursor;
651
+ /** Page size. */
496
652
  limit?: number;
497
653
  /** Comma-separated product-type ids to restrict the browse to. */
498
654
  types?: string;
@@ -525,7 +681,14 @@ export interface MatchInput {
525
681
  }
526
682
  /** Options for a batch material read. */
527
683
  export interface MaterialsBatchOptions {
684
+ /** Projection depth. `tile` omits fields rather than reporting them absent. */
528
685
  view?: "tile" | "detail";
686
+ /**
687
+ * Which id space `ids` are in — MaterialGraph's own opaque `variantId`
688
+ * (default) or Material Bank's `materialBankId`. Both are opaque; they are NOT
689
+ * interchangeable, and passing one under the other's `idKind` returns
690
+ * `missingIds`, not an error.
691
+ */
529
692
  idKind?: OpenEnum<"variantId" | "materialBankId">;
530
693
  }
531
694
  /** Options for a pairings read. */
@@ -533,6 +696,12 @@ export interface PairingsOptions {
533
696
  /** Comma-separated coarse category display names; filters both arms. */
534
697
  categories?: string;
535
698
  limit?: number;
699
+ /** Graph-owned order applied within fixed cospec → considered → colour tiers. */
700
+ order?: PairingOrderMode;
701
+ /** Optional Material Bank/orderable gate; requires an explicit `order`. */
702
+ corpus?: PairingCorpus;
703
+ /** The previous response's `nextCursor`, passed back verbatim. */
704
+ cursor?: Cursor;
536
705
  }
537
706
  /** Options for `catalog/attributes/resolve`. */
538
707
  export interface ResolveAttributeOptions {
@@ -543,184 +712,28 @@ export interface ResolveAttributeOptions {
543
712
  export interface BySpecOptions {
544
713
  limit?: number;
545
714
  }
546
- /** A resolved seed material (identity block shared by the demand reads). */
547
- export interface DemandSeed {
548
- variantId: string;
549
- brand: string;
550
- product: string;
551
- color: string | null;
552
- variantName: string | null;
553
- imageUrl: string | null;
554
- brandSlug: string;
555
- productId: string;
556
- }
715
+ export type * from "./demand-types.js";
716
+ /** One quarter's tally. Sparse across a series — an absent quarter is not a zero. */
557
717
  export interface DemandQuarter {
558
718
  quarter: string;
559
719
  specCount: number;
560
720
  projectCount: number;
561
721
  }
562
- /** Headline demand stats for a seed: counts, quarterly series, momentum. */
563
- export interface MaterialDemandStats {
564
- specCount: number;
565
- projectCount: number;
566
- customerCount: number;
567
- byQuarter: DemandQuarter[];
568
- momentum: DemandMomentum;
569
- }
570
- /** `getMaterialDemand` result, passed through verbatim by the `/demand` route. */
571
- export interface MaterialDemandResult {
572
- found: boolean;
573
- query: string;
574
- seed: DemandSeed | null;
575
- window: "trailing_36mo";
576
- demand: MaterialDemandStats | null;
577
- }
578
722
  /**
579
- * `getCoSpecifiedMaterials` result, passed through verbatim. The `companions`
580
- * list (materials specified alongside the seed) is included but left loosely
581
- * typed — there is no captured fixture to pin its full shape.
723
+ * Which behavioural signal a demand read is scoped to — and therefore what a
724
+ * number drawn from it may honestly be called.
725
+ *
726
+ * - `sampled` — real ORDER co-occurrence. The default everywhere. Present as
727
+ * "specified together" / "specified on N projects".
728
+ * - `considered` — board / moodboard CURATION. Somebody assembled them; nobody
729
+ * is known to have bought them. Present as "considered alongside", never as
730
+ * a specification.
731
+ * - `specified` — the won tier. STATS ONLY: no co-specification edges are built
732
+ * at this grain, so asking for `specified` companions is a rejected request,
733
+ * not an empty result.
582
734
  */
583
- export interface CoSpecifiedResult {
584
- found: boolean;
585
- resolution: OpenEnum<"resolved" | "ambiguous" | "not_found">;
586
- query: string;
587
- seed: DemandSeed | null;
588
- seedCategory: string | null;
589
- seedAlternatives: DemandSeed[];
590
- companions?: unknown[];
591
- }
592
- /** The `/api/v1/demand` envelope — both domain reads side by side, verbatim. */
593
- export interface DemandResponse {
594
- query: string;
595
- demand: MaterialDemandResult;
596
- cospec: CoSpecifiedResult;
597
- }
598
- /** Which behavioural signal candidate demand is read from. */
599
735
  export type DemandSignal = OpenEnum<"sampled" | "considered" | "specified">;
600
- /**
601
- * Body for `POST /api/v1/demand/batch` — enrich 1–48 already-selected candidate
602
- * ids with their real specification demand, in input order and cardinality.
603
- */
604
- export interface DemandBatchInput {
605
- /** 1–48 canonical variant ids. Order AND cardinality are preserved — duplicates are echoed, never collapsed. */
606
- variantIds: string[];
607
- /** The behavioural signal to read demand from. Defaults to `sampled` (order co-occurrence) server-side. */
608
- signal?: DemandSignal;
609
- /** Optional declared context (e.g. `{ projectType: "hospitality" }`) — echoed for provenance, never used to rank or filter. */
610
- context?: Record<string, string>;
611
- }
612
- /** Headline population behind the metric — this variant's own counts. */
613
- export interface DemandBatchPopulation {
614
- specCount: number;
615
- projectCount: number;
616
- customerCount: number;
617
- }
618
- /** The consideration→specification funnel (project-grain; `exclusionRate` is annotate-only). */
619
- export interface DemandBatchFunnel {
620
- consideredProjects: number;
621
- specifiedProjects: number;
622
- excludedCount: number;
623
- exclusionRate: number;
624
- }
625
- /** Per-result guardrail status — present on `internal` (`mg:admin`) results only. */
626
- export interface DemandBatchEvidence {
627
- belowPrivacyFloor: boolean;
628
- belowQualityFloor: boolean;
629
- suppressedForPublic: boolean;
630
- }
631
- /** A variant that carries a usable demand signal. */
632
- export interface DemandBatchAvailableResult {
633
- variantId: string;
634
- state: "available";
635
- population: DemandBatchPopulation;
636
- /** Exact per-quarter counts (sparse — zero-spec quarters are absent). */
637
- intervals: DemandQuarter[];
638
- momentum: DemandMomentum;
639
- /** The funnel; null when the signal is not `sampled` or no `considered` row/session exists. */
640
- funnel: DemandBatchFunnel | null;
641
- /** Exact guardrail evidence for `internal` callers; null for public. */
642
- evidence: DemandBatchEvidence | null;
643
- }
644
- /** A variant with a stats row that is below the public quality floor. */
645
- export interface DemandBatchInsufficientResult {
646
- variantId: string;
647
- state: "insufficient_data";
648
- }
649
- /** A variant with no stats — indistinguishable from a privacy-suppressed one for public callers. */
650
- export interface DemandBatchNoSignalResult {
651
- variantId: string;
652
- state: "no_signal";
653
- }
654
- /** A requested id that resolves to no real variant. */
655
- export interface DemandBatchUnknownResult {
656
- variantId: string;
657
- state: "unknown_variant";
658
- }
659
- /** One discriminated result per input id, in input order and cardinality. */
660
- export type DemandBatchResult = DemandBatchAvailableResult | DemandBatchInsufficientResult | DemandBatchNoSignalResult | DemandBatchUnknownResult;
661
- /** The `POST /api/v1/demand/batch` envelope. */
662
- export interface DemandBatchResponse {
663
- /** The completed ingestion-session id these counts were mined in (the dataset generation). */
664
- generationId: string;
665
- metric: "specification_demand";
666
- /** The metric universe — the window and signal the counts are scoped to. */
667
- universe: {
668
- window: "trailing_36mo";
669
- signal: DemandSignal;
670
- description: string;
671
- };
672
- /** The guardrail status under which `results` were projected. */
673
- guardrail: {
674
- audience: OpenEnum<"public" | "internal">;
675
- minProjectCount: number;
676
- minCustomerCount: number;
677
- };
678
- /** Outcome tallies over `results` — never a denominator for the metric. */
679
- coverage: {
680
- requested: number;
681
- available: number;
682
- insufficientData: number;
683
- noSignal: number;
684
- unknownVariant: number;
685
- };
686
- /** The echoed declared context, or null when none was supplied. */
687
- context: Record<string, string> | null;
688
- results: DemandBatchResult[];
689
- }
690
- export interface CoordinateSeed extends DemandSeed {
691
- /** The input reference this seed resolved from. */
692
- resolvedFrom: string;
693
- }
694
- export interface CoordinateCompanion {
695
- variantId: string;
696
- brand: string;
697
- product: string;
698
- color: string | null;
699
- variantName: string | null;
700
- imageUrl: string | null;
701
- brandSlug: string;
702
- productId: string;
703
- totalLift: number;
704
- liftLabel: string;
705
- seedMatchCount: number;
706
- seedContributions: Array<{
707
- seedVariantId: string;
708
- lift: number;
709
- }>;
710
- demand: CompanionDemand | null;
711
- swatches: Array<{
712
- hex: string;
713
- proportion: number;
714
- }>;
715
- /** Annotate-only appearance similarity — never reorders companions in v1. */
716
- colourFit: number | null;
717
- }
718
- export interface CoordinateResponse {
719
- seeds: CoordinateSeed[];
720
- unresolved: string[];
721
- window: "trailing_36mo";
722
- companions: CoordinateCompanion[];
723
- }
736
+ export type * from "./demand-batch-types.js";
724
737
  export interface ExtractedPalette {
725
738
  colors: Array<{
726
739
  hex: string;
@@ -848,18 +861,6 @@ export interface BrandProfileResponse {
848
861
  name: string;
849
862
  }>;
850
863
  }
851
- /** Seed a demand read by EXACTLY ONE of `variantId` or `material`. */
852
- export interface DemandParams {
853
- variantId?: string;
854
- material?: string;
855
- }
856
- /** Body for `POST /api/v1/pairings/coordinate`. */
857
- export interface CoordinateInput {
858
- materials?: string[];
859
- variantIds?: string[];
860
- projectType?: string;
861
- limit?: number;
862
- }
863
864
  /** Body for `POST /api/v1/search/palettes/extract`. */
864
865
  export interface ExtractPaletteInput {
865
866
  /** The query image as a data URI or bare base64 string. */