@instruments/materialgraph 0.10.0 → 0.11.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 (42) hide show
  1. package/browse-types.d.ts +147 -0
  2. package/browse-types.js +1 -0
  3. package/by-colour-types.d.ts +56 -0
  4. package/client-browse.d.ts +13 -3
  5. package/client-browse.js +12 -2
  6. package/client-exploration.d.ts +9 -0
  7. package/client-exploration.js +7 -0
  8. package/client-source-material-comments.d.ts +51 -0
  9. package/client-source-material-comments.js +40 -0
  10. package/client.d.ts +28 -22
  11. package/client.js +33 -24
  12. package/colour-aggregate-types.d.ts +58 -0
  13. package/colours-client.d.ts +8 -2
  14. package/colours-client.js +15 -2
  15. package/composition-client.d.ts +33 -0
  16. package/composition-client.js +27 -0
  17. package/composition-types.d.ts +46 -0
  18. package/coordinate-client.d.ts +23 -0
  19. package/coordinate-client.js +25 -0
  20. package/exploration-types.d.ts +110 -0
  21. package/exploration-types.js +1 -0
  22. package/fan-deck-types.d.ts +4 -1
  23. package/index.d.ts +4 -1
  24. package/operations.d.ts +1 -1
  25. package/operations.js +25 -0
  26. package/package.json +1 -1
  27. package/palette-search-types.d.ts +31 -0
  28. package/run-types.d.ts +132 -0
  29. package/runs.d.ts +35 -1
  30. package/runs.js +61 -1
  31. package/schedule-types.d.ts +128 -0
  32. package/schema-resource-types.d.ts +1 -1
  33. package/search-refinement-types.d.ts +28 -1
  34. package/search-request-types.d.ts +172 -0
  35. package/search-request-types.js +1 -0
  36. package/slot-pool-types.d.ts +370 -0
  37. package/slot-pool-types.js +1 -0
  38. package/source-material-validation.d.ts +5 -1
  39. package/source-material-validation.js +111 -4
  40. package/types.d.ts +149 -199
  41. package/validation.d.ts +12 -0
  42. package/validation.js +23 -0
package/types.d.ts CHANGED
@@ -1,35 +1,7 @@
1
- /**
2
- * Hand-mirrored wire types for the MaterialGraph public API (spec 0.5.0).
3
- *
4
- * These interfaces intentionally have NO runtime dependency on any
5
- * `@materialgraph/*` package — they are a standalone, publishable mirror of the
6
- * shapes in `apps/web/public/openapi.yaml` and the captured fixtures under
7
- * `docs/surface/fixtures/v0.5.0/`. The `contract-parity` test re-checks them
8
- * against the real Zod schemas so the mirror cannot silently drift.
9
- * TOLERANT-PARSING CONTRACT: the public surface declares open response enums —
10
- * "clients must tolerate unknown enum values and unknown fields". Every union
11
- * that mirrors a server enum is therefore widened with `(string & {})` so a new
12
- * server value still type-checks, and objects that may grow fields are read
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.
27
- */
28
- import type { ByColourSwatch } from "./by-colour-types.js";
29
1
  import type { BySpecVerdict } from "./by-spec-types.js";
30
2
  import type { Cursor, ProductId, VariantId } from "./identifiers.js";
31
3
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
32
- import type { SearchFilter, SearchSuggestion } from "./search-refinement-types.js";
4
+ import type { SearchDisambiguation, SearchSuggestion } from "./search-refinement-types.js";
33
5
  /** A widened string-literal union: known members autocomplete, unknown values still type-check. */
34
6
  export type OpenEnum<T extends string> = T | (string & {});
35
7
  /**
@@ -104,6 +76,15 @@ export interface VisualMeaning {
104
76
  version?: string;
105
77
  appliedAt?: string;
106
78
  }
79
+ /** One member of a collapsed result's {@link SearchResultItem.anatomy}. */
80
+ export interface SearchAnatomyMember {
81
+ /** Opaque — round-trip verbatim. */
82
+ variantId: VariantId;
83
+ variantName: string | null;
84
+ color: string | null;
85
+ score: number;
86
+ signals: SearchResultSignals;
87
+ }
107
88
  /**
108
89
  * One material in a search / match result list.
109
90
  *
@@ -154,6 +135,38 @@ export interface SearchResultItem {
154
135
  hasImage: boolean;
155
136
  palette: PaletteSwatch[];
156
137
  colourMeta?: ColourMeta;
138
+ /**
139
+ * Present when this material's imagery carries nothing beyond ONE colour, so
140
+ * a surface should render that colour rather than the photograph — a paint's
141
+ * photo is a flat fill, and the colour we hold structured beats a JPEG of it.
142
+ *
143
+ * The hex is EXTRACTED FROM the product image (a visual likeness), never a
144
+ * manufacturer-declared colour specification; do not present it as one.
145
+ * ABSENT means the question was not established (unknown product type,
146
+ * unextracted or multi-swatch palette) and the image should be rendered — it
147
+ * NEVER asserts "this is not a coating".
148
+ */
149
+ colourChip?: {
150
+ hex: string;
151
+ };
152
+ /**
153
+ * EXPLAIN-only: the two identities a COLLAPSED result holds, which are not the
154
+ * same thing and are routinely confused.
155
+ *
156
+ * - `representative` — the RANKING identity. Its score is the group's cursor
157
+ * key and the item's `signals` describe IT, not the photograph on the card.
158
+ * Omitted when the face IS the representative.
159
+ * - the item's own `variantId` — the DISPLAY identity (the face).
160
+ *
161
+ * `siblingsTruncated` says the sibling list was capped: a product can hold
162
+ * hundreds of colourways, so absence of a sibling is never evidence it does
163
+ * not exist.
164
+ */
165
+ anatomy?: {
166
+ representative?: SearchAnatomyMember;
167
+ siblings?: SearchAnatomyMember[];
168
+ siblingsTruncated?: boolean;
169
+ };
157
170
  /** Fused score — range [0, sum of active weights]; see `maxScore`. */
158
171
  score: number;
159
172
  /**
@@ -238,11 +251,17 @@ export interface SearchCardItem {
238
251
  * A feed total with its honesty basis — THE CLAIM ABOUT THE MATCH SET, which is
239
252
  * a different number from `results.length` (this page).
240
253
  *
241
- * `exact` — the true count (≤ the cap of 1,000, or a facet-MV read). Say "473
242
- * materials match".
254
+ * `exact` — the true count. Say "473 materials match".
243
255
  * `lowerBound` — a floor (cap hit, ANN-pool bound, or a degraded count). Say
244
256
  * "at least 1,000", never "1,000". Never a silent approximation.
245
257
  *
258
+ * The CAP differs by lane, and only `lowerBound` ever tells you one was hit. The
259
+ * SEARCH feed counts exactly to 1,000 and reports a floor beyond it, because it
260
+ * counts over attribute and tsquery universes where an unbounded count is a
261
+ * timeout. The BROWSE feed ({@link BrowseCounts}) is uncapped — a plain count
262
+ * over the same predicate its page scan ran, so a category of 66,000 reports
263
+ * 66,000 exactly. Read `basis`; never assume a ceiling.
264
+ *
246
265
  * A response with no `counts` at all states no total: `count: true` is opt-in,
247
266
  * and the absence of a total is not a total of zero.
248
267
  */
@@ -259,6 +278,54 @@ export interface SearchCounts {
259
278
  variants: CountedTotal;
260
279
  products?: CountedTotal;
261
280
  }
281
+ /**
282
+ * One value within a facet's counts: the CANONICAL token (send it back verbatim
283
+ * as a filter term), its display label, and how many variants in the counted
284
+ * set carry it.
285
+ */
286
+ export interface SearchFacetValue {
287
+ value: string;
288
+ label: string;
289
+ variants: number;
290
+ }
291
+ /**
292
+ * One facet's counts. `basis` is the honesty label, and — per the negotiated
293
+ * MG-1010 contract — its union is CLOSED: exactly `"exact"` or `"lowerBound"`,
294
+ * never widened, which is why it is not an {@link OpenEnum}. `exact` — each
295
+ * `variants` figure is a true count under the query's whole predicate
296
+ * (filter-driven queries only). `lowerBound` — counted over the ranked
297
+ * candidate pool (`SearchFacets.poolSize`), so each figure is a floor; a
298
+ * pool-counted value is NEVER `exact`. `truncated` is ALWAYS present: true
299
+ * when more distinct values existed beyond the reported list.
300
+ */
301
+ export interface SearchFacetCount {
302
+ values: SearchFacetValue[];
303
+ truncated: boolean;
304
+ basis: "exact" | "lowerBound";
305
+ }
306
+ /**
307
+ * A requested facet that was not counted, with its reason: `"unsupported"` —
308
+ * the id is known but facet counting does not cover it; `"timeout"` — the
309
+ * facet stage's own time budget expired, and the RESULTS are unaffected (the
310
+ * rail could not count; the page is intact). Render it as "couldn't count",
311
+ * never as zero.
312
+ */
313
+ export interface SearchAbsentFacet {
314
+ facetId: string;
315
+ reason: OpenEnum<"unsupported" | "timeout">;
316
+ }
317
+ /**
318
+ * Per-query facet counts (MG-1010) — present ONLY when the request sent
319
+ * `facetCounts`. `counts` is keyed by requested facet id; every requested
320
+ * facet not in `absent` has an entry, including empty ones. `poolSize` is
321
+ * present exactly when the counts are pool-derived.
322
+ */
323
+ export interface SearchFacets {
324
+ poolSize?: number;
325
+ requested: string[];
326
+ absent: SearchAbsentFacet[];
327
+ counts: Record<string, SearchFacetCount>;
328
+ }
262
329
  /** The single candidate source a query resolved to. */
263
330
  export type SearchMode = OpenEnum<"image" | "colour" | "text" | "lexical">;
264
331
  /** Rerank-term report (absent in colour mode — there colour IS the source). */
@@ -290,8 +357,42 @@ export interface SearchResponse {
290
357
  cursor?: Cursor;
291
358
  rerank?: SearchRerankReport;
292
359
  counts?: SearchCounts;
360
+ /**
361
+ * The `#rrggbb` colours the colour signal ACTUALLY ran on, after
362
+ * minor-proportion accents were dropped. Present whenever the request carried
363
+ * explicit `colours`. Compare against your request: a difference means the
364
+ * search that ran is not the search that was asked for.
365
+ */
366
+ effectiveColours?: string[];
367
+ /**
368
+ * Colours removed from the query for carrying a `weight` below the 5% accent
369
+ * threshold. EMPTY is the normal case. Non-empty and the dropped colours are
370
+ * absent from colour retrieval and ranking entirely — they did not merely
371
+ * rank low. To keep a small accent in the query, give it `role: "accent"`.
372
+ */
373
+ droppedColours?: string[];
374
+ /** Per-query facet counts — present ONLY when the request sent `facetCounts`. */
375
+ facets?: SearchFacets;
293
376
  note?: string;
294
377
  timings?: Record<string, number>;
378
+ /** A detected mood term overridden by a stronger colour reading. */
379
+ supersededMoodId?: string;
380
+ /**
381
+ * Present when the query had TWO defensible readings. Says which one ran and
382
+ * offers the other as a complete executable request.
383
+ */
384
+ disambiguation?: SearchDisambiguation;
385
+ /**
386
+ * The corpus this page AND its `counts` were drawn from — present ONLY when
387
+ * the request narrowed it. ABSENT means the whole graph: every material
388
+ * MaterialGraph knows, orderable or not. Read it alongside `counts`: a short
389
+ * page under a scope is a small SCOPED corpus, not a truncated one.
390
+ */
391
+ corpusScope?: {
392
+ scope: OpenEnum<"all" | "materialBankOrderable">;
393
+ /** What the scope admits, in one sentence, for a human reader. */
394
+ description: string;
395
+ };
295
396
  /** Echo of the resolved query input (raw image bytes/vector are never echoed). */
296
397
  query?: Record<string, unknown>;
297
398
  /** A `getVariantBrief` echo of the seed variant, present only in `variantId` seed mode. */
@@ -302,47 +403,6 @@ export interface SearchCardResponse extends Omit<SearchResponse, "view" | "resul
302
403
  view: OpenEnum<"card">;
303
404
  results: SearchCardItem[];
304
405
  }
305
- /** One item from the pure colour-scalar browse. */
306
- export interface BrowseResultItem {
307
- /** Opaque — round-trip verbatim. */
308
- variantId: VariantId;
309
- brand: string;
310
- product: string;
311
- brandSlug: string | null;
312
- productId: ProductId | null;
313
- color: string | null;
314
- variantName: string | null;
315
- imageUrl: string | null;
316
- scalars: {
317
- l: number | null;
318
- chroma: number | null;
319
- vibrancy: number | null;
320
- };
321
- retired?: boolean;
322
- /**
323
- * The row's own image palette — the FULL stored palette of the SAME image
324
- * `imageUrl` serves, uncapped and in stored order, each swatch with `oklab`
325
- * (see {@link ByColourSwatch}), so a card can render, sort or group colours
326
- * without a second call. A one-swatch palette means the extractor found one
327
- * colour (solid-colour materials are genuinely monochrome), not that the list
328
- * was trimmed. ABSENT when no palette is on record for that image: absent
329
- * means unknown, NOT that the material has no colours. These swatches come
330
- * from product imagery, not from the manufacturer.
331
- */
332
- palette?: ByColourSwatch[];
333
- }
334
- /**
335
- * A single page of the colour browse — the SDK's one genuinely cursor-paginated
336
- * surface. `results.length` is this page and the browse states no total at all,
337
- * so a corpus claim can only come from walking every page
338
- * (`client.browse.pages(...)` does the loop).
339
- */
340
- export interface BrowseResponse {
341
- /** THIS PAGE. */
342
- results: BrowseResultItem[];
343
- /** Opaque keyset for the next page. Absent = the walk is finished. */
344
- cursor?: Cursor;
345
- }
346
406
  export type ColourDictionaryId = OpenEnum<"colour_family" | "colour_mood" | "colour_undertone">;
347
407
  export interface ColourFacetTerm {
348
408
  id: string;
@@ -415,6 +475,22 @@ export interface CospecPairing {
415
475
  imageUrl: string | null;
416
476
  brandSlug: string;
417
477
  productId: ProductId;
478
+ /**
479
+ * How this companion relates to the seed — the distinction raw order evidence
480
+ * does not carry, and the one that decides what you may claim.
481
+ *
482
+ * - `companion` — a DIFFERENT BRAND entirely. This is the cross-brand claim
483
+ * the rail exists to make.
484
+ * - `brand_sibling` — the seed's own brand, a different product.
485
+ * - `colourway` — the seed's own product, restyled.
486
+ *
487
+ * **70.6% of edges are same-brand**, so a consumer that ignores this reports
488
+ * the seed's own family as cross-brand intelligence (MG-1007).
489
+ *
490
+ * Optional because responses captured before the field existed are genuinely
491
+ * in the wild; absent means "from an older version", never "unrelated".
492
+ */
493
+ relation?: "companion" | "brand_sibling" | "colourway";
418
494
  category: string | null;
419
495
  projectCount: number;
420
496
  customerCount: number;
@@ -558,133 +634,7 @@ export type SearchImageInput = {
558
634
  type: "base64";
559
635
  data: string;
560
636
  };
561
- /**
562
- * The POST `/search` JSON body — the `searchMaterials` contract. Every field is
563
- * optional; supply at least one signal (`text`, `colours`, `mood`, `similarTo`,
564
- * or `image`). Passed through to the server verbatim.
565
- */
566
- export interface SearchInput {
567
- text?: string;
568
- textModes?: Array<OpenEnum<"visual" | "lexical">>;
569
- colours?: ColourInput[];
570
- colourLogic?: OpenEnum<"all" | "any">;
571
- colourTolerance?: OpenEnum<"tight" | "normal" | "loose">;
572
- colourAppearance?: {
573
- lightness?: {
574
- min: number;
575
- max: number;
576
- };
577
- warmth?: {
578
- min: number;
579
- max: number;
580
- };
581
- };
582
- mood?: string;
583
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
584
- /**
585
- * Seed a visual search from an existing variant's image (`variantId`), from a
586
- * Material Bank SKU (`materialBankId`), or from a pre-computed image vector
587
- * (`vector`).
588
- *
589
- * `materialBankId` exists so a caller already holding a SKU does not have to
590
- * ask us to translate it before it can ask us anything useful — one call, not
591
- * two. It resolves to the variant server-side and then behaves identically to
592
- * the `{ variantId }` form, seed verdicts included. A SKU that names no
593
- * variant is a 400 carrying the offending value: never an empty result set,
594
- * never a silent fall back to unseeded search.
595
- *
596
- * The `{ vector }` form carries no seed variant, so it is the only one
597
- * `verdicts` cannot diff a candidate against.
598
- */
599
- similarTo?: {
600
- variantId: VariantId;
601
- } | {
602
- materialBankId: string;
603
- } | {
604
- vector: string;
605
- };
606
- image?: SearchImageInput;
607
- category?: {
608
- boost?: string;
609
- filter?: string[];
610
- };
611
- weights?: {
612
- image?: number;
613
- category?: number;
614
- colour?: number;
615
- lexical?: number;
616
- /** Text rerank multiplier applied when a seeded search also carries `text` (default 0.4). */
617
- text?: number;
618
- };
619
- filters?: SearchFilter[];
620
- /**
621
- * SOFT spec criteria (rank-never-exclude). Same shape as `filters`; a preference
622
- * never removes a result — it adds a `preferred` cell to each hit's `verdict` and
623
- * demotes a failed/missing preferred hit WITHIN its page only.
624
- */
625
- preferences?: SearchFilter[];
626
- /** Page size — the size of ONE page, never a bound on the match set. */
627
- limit?: number;
628
- /** The previous response's `cursor`, passed back verbatim. Never synthesise one. */
629
- cursor?: Cursor;
630
- explain?: boolean;
631
- collapse?: OpenEnum<"none" | "product">;
632
- /**
633
- * Opt-in feed total: EXACT up to 1,000, `lowerBound` beyond / for ANN pools.
634
- * Without it the response states NO total — and a page size is not one.
635
- */
636
- count?: boolean;
637
- /** Only return materials with at least one catalog image. Default false (full catalog). */
638
- hasImages?: boolean;
639
- /**
640
- * Opt-in seed-relative verdict cells (MG-629). Only meaningful with
641
- * `similarTo: { variantId }`: each hit's `verdict` gains `preferred` cells
642
- * diffing its comparable attributes (numeric + a `colour` axis) against the
643
- * seed's, with directional deviations. Inert on every other seed form.
644
- */
645
- verdicts?: boolean;
646
- /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
647
- view?: OpenEnum<"full" | "card">;
648
- }
649
- /** Query-string params for the GET `/search` form. */
650
- export interface SearchGetParams {
651
- q?: string;
652
- colours?: string;
653
- logic?: OpenEnum<"all" | "any">;
654
- mood?: string;
655
- sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
656
- tol?: OpenEnum<"tight" | "normal" | "loose">;
657
- /** Seed the search from this variant's image. Opaque — pass it through verbatim. */
658
- variantId?: VariantId;
659
- limit?: number;
660
- /** "0" or "1" — include per-signal `signals` on each result. */
661
- explain?: OpenEnum<"0" | "1">;
662
- /** "true" — opt in to a `counts.variants` feed total. */
663
- count?: OpenEnum<"true">;
664
- /** "true" — only return materials with at least one catalog image. */
665
- hasImages?: OpenEnum<"true">;
666
- /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
667
- view?: OpenEnum<"full" | "card">;
668
- }
669
- /** Params for the pure colour `search/browse` surface. */
670
- export interface BrowseParams {
671
- sort: OpenEnum<"light" | "dark" | "vivid" | "muted" | "colourful">;
672
- /**
673
- * The previous page's `cursor`, verbatim. Opaque keyset state — not an offset.
674
- * Changing `sort`, `limit` or `types` invalidates it.
675
- */
676
- cursor?: Cursor;
677
- /** Page size. */
678
- limit?: number;
679
- /**
680
- * Comma-separated taxonomy keys to restrict the browse to, at EITHER tier of
681
- * `/api/v1/taxonomy/categories`. A GROUP key (`tile_surfaces`) expands to
682
- * every product type under it, a TYPE key (`ceramic_porcelain_tile`) stands
683
- * for itself, and a mixed list is their deduplicated union. A key in neither
684
- * tier is a 400 listing the valid keys — never an empty page.
685
- */
686
- types?: string;
687
- }
637
+ export type { SearchGetParams, SearchInput } from "./search-request-types.js";
688
638
  /** Options for a batch material read. */
689
639
  export interface MaterialsBatchOptions {
690
640
  /** Projection depth. `tile` omits fields rather than reporting them absent. */
package/validation.d.ts CHANGED
@@ -34,6 +34,18 @@ export declare function requireOptionalString(value: unknown, what: string, fail
34
34
  export declare function requireNullableString(value: unknown, what: string, fail: ValidationFailure): string | null;
35
35
  /** A `z.string().min(1).nullable()`. */
36
36
  export declare function requireNullableNonEmptyString(value: unknown, what: string, fail: ValidationFailure): string | null;
37
+ /**
38
+ * A `z.string().trim().min(1).max(max)`.
39
+ *
40
+ * The `.trim()` is a TRANSFORM, so the schema's parsed output is the trimmed
41
+ * string and its length bounds apply AFTER trimming. A mirror that checked the
42
+ * raw length would disagree with the schema twice over: it would accept a
43
+ * whitespace-only value the schema rejects, and reject a padded value the schema
44
+ * accepts. Returns the trimmed string, which is what the schema returns.
45
+ */
46
+ export declare function requireTrimmedBoundedString(value: unknown, what: string, max: number, fail: ValidationFailure): string;
47
+ /** A `z.string().trim().min(1).max(max).nullable()`. */
48
+ export declare function requireNullableTrimmedBoundedString(value: unknown, what: string, max: number, fail: ValidationFailure): string | null;
37
49
  /** A `z.boolean()`. */
38
50
  export declare function requireBoolean(value: unknown, what: string, fail: ValidationFailure): boolean;
39
51
  /** A `z.boolean().optional()`. */
package/validation.js CHANGED
@@ -55,6 +55,29 @@ export function requireNullableNonEmptyString(value, what, fail) {
55
55
  return null;
56
56
  return requireNonEmptyString(value, what, fail);
57
57
  }
58
+ /**
59
+ * A `z.string().trim().min(1).max(max)`.
60
+ *
61
+ * The `.trim()` is a TRANSFORM, so the schema's parsed output is the trimmed
62
+ * string and its length bounds apply AFTER trimming. A mirror that checked the
63
+ * raw length would disagree with the schema twice over: it would accept a
64
+ * whitespace-only value the schema rejects, and reject a padded value the schema
65
+ * accepts. Returns the trimmed string, which is what the schema returns.
66
+ */
67
+ export function requireTrimmedBoundedString(value, what, max, fail) {
68
+ const text = requireString(value, what, fail).trim();
69
+ if (text.length === 0)
70
+ throw fail(`${what} is empty once trimmed`);
71
+ if (text.length > max)
72
+ throw fail(`${what} is longer than ${max} characters (${text.length})`);
73
+ return text;
74
+ }
75
+ /** A `z.string().trim().min(1).max(max).nullable()`. */
76
+ export function requireNullableTrimmedBoundedString(value, what, max, fail) {
77
+ if (value === null)
78
+ return null;
79
+ return requireTrimmedBoundedString(value, what, max, fail);
80
+ }
58
81
  /** A `z.boolean()`. */
59
82
  export function requireBoolean(value, what, fail) {
60
83
  if (typeof value !== "boolean")