@instruments/materialgraph 0.2.0 → 0.3.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 (45) hide show
  1. package/by-spec-types.d.ts +73 -0
  2. package/by-spec-types.js +7 -0
  3. package/client.d.ts +68 -23
  4. package/client.js +82 -25
  5. package/codes-client.d.ts +82 -0
  6. package/codes-client.js +73 -0
  7. package/codes-types.d.ts +154 -9
  8. package/colour-aggregate-types.d.ts +93 -0
  9. package/colour-aggregate-types.js +1 -0
  10. package/colours-client.d.ts +52 -0
  11. package/colours-client.js +39 -0
  12. package/composition-types.d.ts +228 -2
  13. package/http-transport.d.ts +19 -2
  14. package/http-transport.js +72 -2
  15. package/index.d.ts +7 -0
  16. package/index.js +3 -0
  17. package/match-stream-types.d.ts +150 -0
  18. package/match-stream-types.js +28 -0
  19. package/match-stream.d.ts +22 -0
  20. package/match-stream.js +280 -0
  21. package/material-block-types.d.ts +23 -2
  22. package/material-types.d.ts +92 -0
  23. package/material-types.js +1 -0
  24. package/ndjson.d.ts +46 -0
  25. package/ndjson.js +103 -0
  26. package/operations.d.ts +1 -1
  27. package/operations.js +6 -0
  28. package/package.json +1 -1
  29. package/run-types.d.ts +65 -1
  30. package/runs.d.ts +45 -3
  31. package/runs.js +82 -43
  32. package/schedule-types.d.ts +88 -0
  33. package/schedule-types.js +12 -0
  34. package/search-refinement-types.d.ts +13 -0
  35. package/search-result-validation.d.ts +27 -0
  36. package/search-result-validation.js +174 -0
  37. package/source-material-validation.d.ts +3 -0
  38. package/source-material-validation.js +109 -0
  39. package/spec-types.d.ts +4 -0
  40. package/spec-types.js +4 -0
  41. package/specifications.d.ts +9 -0
  42. package/specifications.js +9 -0
  43. package/types.d.ts +59 -73
  44. package/validation.d.ts +119 -0
  45. package/validation.js +241 -0
package/specifications.js CHANGED
@@ -42,6 +42,11 @@ export function hasSpec(attributeId) {
42
42
  * virtual attribute so a programme matches wherever it landed at ingest
43
43
  * (compliance / sustainability / material-health / VOC buckets).
44
44
  *
45
+ * The value may be a display name or alias ("GREENGUARD Gold", "LEED") — the
46
+ * server resolves it to its canonical programme token before matching — or a
47
+ * canonical token directly (e.g. `greenguard_certificate`,
48
+ * `has_greenguard_gold_certification`).
49
+ *
45
50
  * @example
46
51
  * // "find a GREENGUARD-certified alternative"
47
52
  * client.search({ similarTo: { variantId: seed }, filters: [certificationProgramme("GREENGUARD Gold")] });
@@ -64,6 +69,10 @@ export function specCriterion(attributeId, value) {
64
69
  * An `attribute:value` criterion string for `bySpec` that filters by
65
70
  * certification PROGRAMME across all four certification buckets.
66
71
  *
72
+ * The value may be a display name or alias ("GREENGUARD Gold", "LEED"), resolved
73
+ * server-side to its canonical programme token, or a canonical token directly
74
+ * (e.g. `greenguard_certificate`, `has_greenguard_gold_certification`).
75
+ *
67
76
  * @example
68
77
  * // every brand's GREENGUARD-Gold materials
69
78
  * client.bySpec([certificationProgrammeCriterion("GREENGUARD Gold")]);
package/types.d.ts CHANGED
@@ -12,9 +12,19 @@
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
14
  */
15
+ import type { BySpecVerdict } from "./by-spec-types.js";
16
+ import type { MaterialDetail, MaterialTile } from "./material-types.js";
15
17
  import type { SearchFilter, SearchSuggestion } from "./search-refinement-types.js";
16
18
  /** A widened string-literal union: known members autocomplete, unknown values still type-check. */
17
19
  export type OpenEnum<T extends string> = T | (string & {});
20
+ /**
21
+ * A JSON value, recursively — exactly what survives a `jsonb` round-trip
22
+ * unchanged. Used where a payload's SHAPE belongs to its producer but its values
23
+ * must still be JSON: `Date`, `bigint`, `undefined`, and functions are not.
24
+ */
25
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
26
+ [key: string]: JsonValue;
27
+ };
18
28
  /** One palette swatch of a material's main image. */
19
29
  export interface PaletteSwatch {
20
30
  hex: string;
@@ -48,60 +58,15 @@ export interface MaterialColour {
48
58
  }
49
59
  /** Where the served image came from. */
50
60
  export type ImageSource = OpenEnum<"r2" | "origin">;
51
- /** The grid-card projection of a material. */
52
- export interface MaterialTile {
53
- variantId: string;
54
- brand: string;
55
- product: string;
56
- variantName: string | null;
57
- imageUrl: string | null;
58
- imageSource: ImageSource | null;
59
- category: string | null;
60
- colour: MaterialColour;
61
- }
62
- /** A canonical attribute/term pair carried on a detail record. */
63
- export interface MaterialFacet {
64
- attributeId: string;
65
- term: string;
66
- }
67
- /**
68
- * One dimensional configuration of a material. Numeric fields are millimetres
69
- * (mass in kilograms); string fields are the source's own descriptors. The
70
- * index signature preserves the corpus's long tail of rarer keys.
71
- */
72
- export interface DimensionEntry {
73
- label?: string;
74
- tile_size?: string;
75
- measurement_basis?: string;
76
- width_mm?: number;
77
- length_mm?: number;
78
- height_mm?: number;
79
- depth_mm?: number;
80
- thickness_mm?: number;
81
- diameter_mm?: number;
82
- visible_projection_mm?: number;
83
- weight_kg?: number;
84
- [key: string]: unknown;
85
- }
86
- /** The record projection of a material — the tile plus detail-only fields. */
87
- export interface MaterialDetail extends MaterialTile {
88
- brandSlug: string;
89
- productId: string;
90
- color: string | null;
91
- materialBankId: string | null;
92
- installationImageUrl: string | null;
93
- displayCategory: string | null;
94
- categoryPath: string[];
95
- palette: PaletteSwatch[];
96
- facets: MaterialFacet[];
97
- dimensions: DimensionEntry[] | null;
98
- }
61
+ export type { DimensionEntry, MaterialAvailability, MaterialDetail, MaterialFacet, MaterialSuccessor, MaterialTile, } from "./material-types.js";
99
62
  /** Per-signal scores, present on a result when `explain` was requested. */
100
63
  export interface SearchResultSignals {
101
64
  image: number | null;
102
65
  category: number | null;
103
66
  colour: number | null;
104
67
  lexical: number | null;
68
+ /** Text→image RERANK cosine — non-null only for a seeded search carrying `text`. */
69
+ text: number | null;
105
70
  }
106
71
  /** An accepted/measured visual meaning — descriptive evidence, never a ranking cause. */
107
72
  export interface VisualMeaning {
@@ -131,8 +96,21 @@ export interface SearchResultItem {
131
96
  score: number;
132
97
  signals?: SearchResultSignals;
133
98
  retired?: boolean;
99
+ /**
100
+ * Present (true) only on quality-floor rejects returned under `includeBelowFloor`
101
+ * (match only) — the item failed the match image-quality floor and would be
102
+ * dropped by default. Appended after the kept results.
103
+ */
104
+ belowFloor?: boolean;
134
105
  siblingCount?: number;
135
106
  visualMeanings?: VisualMeaning[];
107
+ /**
108
+ * The shared report card for this hit against the request's `filters` (required
109
+ * cells) and `preferences` (preferred cells). Present (full view only) whenever a
110
+ * spec filter or preference was given. Reuses the by-spec verdict shape — the
111
+ * shared CandidateVerdict mirror; see {@link MaterialVerdict}.
112
+ */
113
+ verdict?: BySpecVerdict;
136
114
  }
137
115
  /** The lean `view:"card"` result item — identity + name + thumbnail + score. */
138
116
  export interface SearchCardItem {
@@ -385,31 +363,7 @@ export interface ResolveAttributeResponse {
385
363
  candidates: AttributeCandidate[];
386
364
  }
387
365
  export type * from "./catalog-discovery-types.js";
388
- export interface BySpecMatchedCell {
389
- attributeId: string;
390
- confidence: number | null;
391
- sourceBasis: string | null;
392
- }
393
- export interface BySpecMatch {
394
- variantId: string;
395
- brand: string;
396
- product: string;
397
- variant: string;
398
- color: string | null;
399
- imageUrl: string | null;
400
- hex: string | null;
401
- matched: BySpecMatchedCell[];
402
- minConfidence: number | null;
403
- }
404
- export interface BySpecResponse {
405
- criteria: Array<{
406
- attribute: string;
407
- value: string;
408
- }>;
409
- totalMatches: number;
410
- brandsSpanned: number;
411
- matches: BySpecMatch[];
412
- }
366
+ export type * from "./by-spec-types.js";
413
367
  export interface WhoamiResponse {
414
368
  principal: string;
415
369
  scopes: string[];
@@ -453,6 +407,11 @@ export interface SearchInput {
453
407
  };
454
408
  mood?: string;
455
409
  sort?: OpenEnum<"relevance" | "light" | "dark" | "vivid" | "muted" | "colourful">;
410
+ /**
411
+ * Seed a visual search from an existing variant's image (`variantId`) or a
412
+ * pre-computed image vector (`vector`). Only the `{ variantId }` form carries a
413
+ * seed variant, so it is the only form `verdicts` can diff a candidate against.
414
+ */
456
415
  similarTo?: {
457
416
  variantId: string;
458
417
  } | {
@@ -468,8 +427,16 @@ export interface SearchInput {
468
427
  category?: number;
469
428
  colour?: number;
470
429
  lexical?: number;
430
+ /** Text rerank multiplier applied when a seeded search also carries `text` (default 0.4). */
431
+ text?: number;
471
432
  };
472
433
  filters?: SearchFilter[];
434
+ /**
435
+ * SOFT spec criteria (rank-never-exclude). Same shape as `filters`; a preference
436
+ * never removes a result — it adds a `preferred` cell to each hit's `verdict` and
437
+ * demotes a failed/missing preferred hit WITHIN its page only.
438
+ */
439
+ preferences?: SearchFilter[];
473
440
  limit?: number;
474
441
  cursor?: string;
475
442
  explain?: boolean;
@@ -478,6 +445,13 @@ export interface SearchInput {
478
445
  count?: boolean;
479
446
  /** Only return materials with at least one catalog image. Default false (full catalog). */
480
447
  hasImages?: boolean;
448
+ /**
449
+ * Opt-in seed-relative verdict cells (MG-629). Only meaningful with
450
+ * `similarTo: { variantId }`: each hit's `verdict` gains `preferred` cells
451
+ * diffing its comparable attributes (numeric + a `colour` axis) against the
452
+ * seed's, with directional deviations. Inert on every other seed form.
453
+ */
454
+ verdicts?: boolean;
481
455
  /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
482
456
  view?: OpenEnum<"full" | "card">;
483
457
  }
@@ -534,7 +508,19 @@ export interface MatchInput {
534
508
  detectionLabel?: string;
535
509
  materialFunction?: string;
536
510
  detectionCategory?: string;
511
+ /**
512
+ * Opt-in: also return quality-floor rejects, flagged `belowFloor: true` and
513
+ * appended after the kept results. Absent/false = default behaviour;
514
+ * `droppedBelowFloor` is unchanged.
515
+ */
516
+ includeBelowFloor?: boolean;
537
517
  requestedType?: OpenEnum<"swatch" | "installation" | "flatplan">;
518
+ /** Hard spec gates forwarded verbatim to the inner search — term match or presence, each with an optional trust bar. */
519
+ filters?: SearchFilter[];
520
+ /** Soft criteria (rank-never-exclude). A miss demotes within the page but never removes a hit. */
521
+ preferences?: SearchFilter[];
522
+ /** Opt in to seed-relative verdict cells; inert unless the inner search carries a variant seed. */
523
+ verdicts?: boolean;
538
524
  }
539
525
  /** Options for a batch material read. */
540
526
  export interface MaterialsBatchOptions {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The SDK's shared wire-validation primitives — the vocabulary every hand-rolled
3
+ * mirror of a canonical Zod schema is built from.
4
+ *
5
+ * The SDK publishes with ZERO runtime dependencies, so it cannot call
6
+ * `schema.parse()` on anything. Where a wire payload must be proven rather than
7
+ * trusted, it is checked with these helpers and the result is pinned against the
8
+ * real schema in `contract-parity.test.ts`. Each helper mirrors one Zod
9
+ * construct, named for it, so a reviewer can read the mirror against the schema
10
+ * line by line.
11
+ *
12
+ * Every helper takes the same three shapes: the value, a `what` path used in the
13
+ * diagnostic (`results[3].verdict.cells[0].state`), and a {@link ValidationFailure}
14
+ * owned by the calling module — so one payload's failures all read alike without
15
+ * this file knowing what a Match frame or a Run event is.
16
+ */
17
+ import { MaterialGraphError } from "./errors.js";
18
+ import type { JsonValue } from "./types.js";
19
+ /**
20
+ * Builds the one error a validation run raises. The calling module supplies it,
21
+ * so the message names that payload and quotes its source.
22
+ */
23
+ export type ValidationFailure = (reason: string) => MaterialGraphError;
24
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
25
+ /** A `z.object({…})`. */
26
+ export declare function requireRecord(value: unknown, what: string, fail: ValidationFailure): Record<string, unknown>;
27
+ /** A `z.string()`. */
28
+ export declare function requireString(value: unknown, what: string, fail: ValidationFailure): string;
29
+ /** A `z.string().min(1)`. */
30
+ export declare function requireNonEmptyString(value: unknown, what: string, fail: ValidationFailure): string;
31
+ /** A `z.string().optional()`: absent is fine, present-but-wrong is not. */
32
+ export declare function requireOptionalString(value: unknown, what: string, fail: ValidationFailure): void;
33
+ /** A `z.string().nullable()`. */
34
+ export declare function requireNullableString(value: unknown, what: string, fail: ValidationFailure): string | null;
35
+ /** A `z.string().min(1).nullable()`. */
36
+ export declare function requireNullableNonEmptyString(value: unknown, what: string, fail: ValidationFailure): string | null;
37
+ /** A `z.boolean()`. */
38
+ export declare function requireBoolean(value: unknown, what: string, fail: ValidationFailure): boolean;
39
+ /** A `z.boolean().optional()`. */
40
+ export declare function requireOptionalBoolean(value: unknown, what: string, fail: ValidationFailure): void;
41
+ /**
42
+ * A `z.number()`. Zod 4 rejects `NaN` AND `±Infinity` for it, and `1e999` parses
43
+ * out of JSON as `Infinity`, so finiteness is a reachable check.
44
+ */
45
+ export declare function requireNumber(value: unknown, what: string, fail: ValidationFailure): number;
46
+ /** A `z.number().optional()`. */
47
+ export declare function requireOptionalNumber(value: unknown, what: string, fail: ValidationFailure): void;
48
+ /** A `z.number().nullable()`. */
49
+ export declare function requireNullableNumber(value: unknown, what: string, fail: ValidationFailure): number | null;
50
+ /**
51
+ * A `z.number().min(min).max(max)`. `NaN` and both infinities fail the
52
+ * comparisons, exactly as the bounded Zod schema rejects them.
53
+ */
54
+ export declare function requireBoundedNumber(value: unknown, what: string, fail: ValidationFailure, min: number, max: number): number;
55
+ /** A `z.number().min(min).max(max).nullable()`. */
56
+ export declare function requireNullableBoundedNumber(value: unknown, what: string, fail: ValidationFailure, min: number, max: number): number | null;
57
+ /**
58
+ * A bare `z.number().int()` — integer, finite, and within the SAFE range, with
59
+ * NO lower bound.
60
+ *
61
+ * Measured against the pinned Zod (4.4.3), `.int()` is exactly
62
+ * `Number.isSafeInteger`: it accepts `0`, `-1`, and `±MAX_SAFE_INTEGER`, and
63
+ * rejects `1.5`, `±2**53`, `NaN`, and `±Infinity`.
64
+ *
65
+ * Two ways to get this wrong, both of which break parity in a different
66
+ * direction. Adding a `min: 0` REJECTS a negative the schema accepts — a client
67
+ * refusing a legitimate response. Using `Number.isInteger` ACCEPTS `2**53`,
68
+ * which the schema rejects — so `requireIntegerAtLeast` is not this helper with
69
+ * the bound dropped, and the safe-range test is why.
70
+ */
71
+ export declare function requireSafeInteger(value: unknown, what: string, fail: ValidationFailure): number;
72
+ /** A `z.number().int().min(min)` — the integer constraint AND the lower bound. */
73
+ export declare function requireIntegerAtLeast(value: unknown, what: string, fail: ValidationFailure, min: number): number;
74
+ /** A `z.enum([…])` — CLOSED, exactly like the canonical member list. */
75
+ export declare function requireEnum(value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure): string;
76
+ /** A `z.enum([…]).nullable()`. */
77
+ export declare function requireNullableEnum(value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure): string | null;
78
+ /** A `z.array(…)`. */
79
+ export declare function requireArray(value: unknown, what: string, fail: ValidationFailure): unknown[];
80
+ /**
81
+ * A `z.array(…).default([])`: an OMITTED array is valid and materialises as
82
+ * `[]`; a present non-array is not.
83
+ */
84
+ export declare function requireDefaultedArray(value: unknown, what: string, fail: ValidationFailure): unknown[];
85
+ /** A `z.array(z.string()).min(1)` — non-empty, and every element a string. */
86
+ export declare function requireNonEmptyStringArray(value: unknown, what: string, fail: ValidationFailure): string[];
87
+ /**
88
+ * A `z.json()` — a JSON value, recursively: `string | number | boolean | null`,
89
+ * an array of them, or an object of them, at ANY depth.
90
+ *
91
+ * WHY THIS EXISTS, given it can never fire over the wire. Every SDK response
92
+ * has already been through `JSON.parse`, so a `Date`, `bigint`, `undefined`, or
93
+ * function CANNOT be present by construction — this check is unreachable in
94
+ * practice. It is here for PARITY: the canonical `sourceImageAnalysisResultSchema`
95
+ * uses `z.json()` precisely because those four break differently on the way to
96
+ * `jsonb` (a `bigint` throws in `JSON.stringify`, an `undefined` is silently
97
+ * dropped, and a `Date` round-trips as an ISO *string* — a different type read
98
+ * back than written). The mirror has to reject what the schema rejects or the
99
+ * parity test is lying. Do not delete this as dead code.
100
+ */
101
+ export declare function requireJsonValue(value: unknown, what: string, fail: ValidationFailure): JsonValue;
102
+ /**
103
+ * A `z.record(z.string(), z.json())` — a PLAIN JSON object of JSON values.
104
+ *
105
+ * Deliberately NOT `requireRecord` + walk the entries. That combination has a
106
+ * top-level hole: `Object.entries(new Date())` and `Object.entries(new Map())`
107
+ * are both `[]`, so there is nothing to recurse over and the value is accepted
108
+ * as `{}` — while `z.record(z.string(), z.json())` rejects both outright. A
109
+ * "non-array object" guard would close those two and still miss a class instance
110
+ * with real own properties, which canonical also rejects. Only a prototype test
111
+ * matches, and {@link requireJsonValue} already applies it at every depth — so
112
+ * this delegates there and then narrows to an object, rather than reimplementing
113
+ * the test and acquiring the same hole one level up.
114
+ *
115
+ * `requireRecord` is left alone: its other callers mirror `z.object({…})`, which
116
+ * genuinely does accept any record, and tightening a shared helper under them
117
+ * would be a change none of them asked for.
118
+ */
119
+ export declare function requireJsonRecord(value: unknown, what: string, fail: ValidationFailure): Record<string, JsonValue>;
package/validation.js ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * The SDK's shared wire-validation primitives — the vocabulary every hand-rolled
3
+ * mirror of a canonical Zod schema is built from.
4
+ *
5
+ * The SDK publishes with ZERO runtime dependencies, so it cannot call
6
+ * `schema.parse()` on anything. Where a wire payload must be proven rather than
7
+ * trusted, it is checked with these helpers and the result is pinned against the
8
+ * real schema in `contract-parity.test.ts`. Each helper mirrors one Zod
9
+ * construct, named for it, so a reviewer can read the mirror against the schema
10
+ * line by line.
11
+ *
12
+ * Every helper takes the same three shapes: the value, a `what` path used in the
13
+ * diagnostic (`results[3].verdict.cells[0].state`), and a {@link ValidationFailure}
14
+ * owned by the calling module — so one payload's failures all read alike without
15
+ * this file knowing what a Match frame or a Run event is.
16
+ */
17
+ import { MaterialGraphError } from "./errors.js";
18
+ export function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ /** A `z.object({…})`. */
22
+ export function requireRecord(value, what, fail) {
23
+ if (!isRecord(value))
24
+ throw fail(`${what} is not a JSON object`);
25
+ return value;
26
+ }
27
+ /** A `z.string()`. */
28
+ export function requireString(value, what, fail) {
29
+ if (typeof value !== "string")
30
+ throw fail(`${what} is not a string`);
31
+ return value;
32
+ }
33
+ /** A `z.string().min(1)`. */
34
+ export function requireNonEmptyString(value, what, fail) {
35
+ const text = requireString(value, what, fail);
36
+ if (text.length === 0)
37
+ throw fail(`${what} is an empty string`);
38
+ return text;
39
+ }
40
+ /** A `z.string().optional()`: absent is fine, present-but-wrong is not. */
41
+ export function requireOptionalString(value, what, fail) {
42
+ if (value === undefined)
43
+ return;
44
+ requireString(value, what, fail);
45
+ }
46
+ /** A `z.string().nullable()`. */
47
+ export function requireNullableString(value, what, fail) {
48
+ if (value === null)
49
+ return null;
50
+ return requireString(value, what, fail);
51
+ }
52
+ /** A `z.string().min(1).nullable()`. */
53
+ export function requireNullableNonEmptyString(value, what, fail) {
54
+ if (value === null)
55
+ return null;
56
+ return requireNonEmptyString(value, what, fail);
57
+ }
58
+ /** A `z.boolean()`. */
59
+ export function requireBoolean(value, what, fail) {
60
+ if (typeof value !== "boolean")
61
+ throw fail(`${what} is not a boolean`);
62
+ return value;
63
+ }
64
+ /** A `z.boolean().optional()`. */
65
+ export function requireOptionalBoolean(value, what, fail) {
66
+ if (value === undefined)
67
+ return;
68
+ requireBoolean(value, what, fail);
69
+ }
70
+ /**
71
+ * A `z.number()`. Zod 4 rejects `NaN` AND `±Infinity` for it, and `1e999` parses
72
+ * out of JSON as `Infinity`, so finiteness is a reachable check.
73
+ */
74
+ export function requireNumber(value, what, fail) {
75
+ if (typeof value !== "number" || !Number.isFinite(value)) {
76
+ throw fail(`${what} is not a finite number`);
77
+ }
78
+ return value;
79
+ }
80
+ /** A `z.number().optional()`. */
81
+ export function requireOptionalNumber(value, what, fail) {
82
+ if (value === undefined)
83
+ return;
84
+ requireNumber(value, what, fail);
85
+ }
86
+ /** A `z.number().nullable()`. */
87
+ export function requireNullableNumber(value, what, fail) {
88
+ if (value === null)
89
+ return null;
90
+ return requireNumber(value, what, fail);
91
+ }
92
+ /**
93
+ * A `z.number().min(min).max(max)`. `NaN` and both infinities fail the
94
+ * comparisons, exactly as the bounded Zod schema rejects them.
95
+ */
96
+ export function requireBoundedNumber(value, what, fail, min, max) {
97
+ if (typeof value !== "number" || !(value >= min && value <= max)) {
98
+ throw fail(`${what} is not a number within [${min}, ${max}]`);
99
+ }
100
+ return value;
101
+ }
102
+ /** A `z.number().min(min).max(max).nullable()`. */
103
+ export function requireNullableBoundedNumber(value, what, fail, min, max) {
104
+ if (value === null)
105
+ return null;
106
+ return requireBoundedNumber(value, what, fail, min, max);
107
+ }
108
+ /**
109
+ * A bare `z.number().int()` — integer, finite, and within the SAFE range, with
110
+ * NO lower bound.
111
+ *
112
+ * Measured against the pinned Zod (4.4.3), `.int()` is exactly
113
+ * `Number.isSafeInteger`: it accepts `0`, `-1`, and `±MAX_SAFE_INTEGER`, and
114
+ * rejects `1.5`, `±2**53`, `NaN`, and `±Infinity`.
115
+ *
116
+ * Two ways to get this wrong, both of which break parity in a different
117
+ * direction. Adding a `min: 0` REJECTS a negative the schema accepts — a client
118
+ * refusing a legitimate response. Using `Number.isInteger` ACCEPTS `2**53`,
119
+ * which the schema rejects — so `requireIntegerAtLeast` is not this helper with
120
+ * the bound dropped, and the safe-range test is why.
121
+ */
122
+ export function requireSafeInteger(value, what, fail) {
123
+ if (!Number.isSafeInteger(value))
124
+ throw fail(`${what} is not a safe integer`);
125
+ return requireNumber(value, what, fail);
126
+ }
127
+ /** A `z.number().int().min(min)` — the integer constraint AND the lower bound. */
128
+ export function requireIntegerAtLeast(value, what, fail, min) {
129
+ const numeric = requireNumber(value, what, fail);
130
+ if (!Number.isInteger(numeric))
131
+ throw fail(`${what} is not an integer`);
132
+ if (numeric < min)
133
+ throw fail(`${what} is below the minimum of ${min}`);
134
+ return numeric;
135
+ }
136
+ /** A `z.enum([…])` — CLOSED, exactly like the canonical member list. */
137
+ export function requireEnum(value, allowed, what, fail) {
138
+ const text = requireString(value, what, fail);
139
+ if (!allowed.includes(text)) {
140
+ throw fail(`${what} is not one of ${allowed.join(" | ")} (${text})`);
141
+ }
142
+ return text;
143
+ }
144
+ /** A `z.enum([…]).nullable()`. */
145
+ export function requireNullableEnum(value, allowed, what, fail) {
146
+ if (value === null)
147
+ return null;
148
+ return requireEnum(value, allowed, what, fail);
149
+ }
150
+ /** A `z.array(…)`. */
151
+ export function requireArray(value, what, fail) {
152
+ if (!Array.isArray(value))
153
+ throw fail(`${what} is not an array`);
154
+ return value;
155
+ }
156
+ /**
157
+ * A `z.array(…).default([])`: an OMITTED array is valid and materialises as
158
+ * `[]`; a present non-array is not.
159
+ */
160
+ export function requireDefaultedArray(value, what, fail) {
161
+ if (value === undefined)
162
+ return [];
163
+ return requireArray(value, what, fail);
164
+ }
165
+ /** A `z.array(z.string()).min(1)` — non-empty, and every element a string. */
166
+ export function requireNonEmptyStringArray(value, what, fail) {
167
+ const items = requireArray(value, what, fail);
168
+ if (items.length === 0)
169
+ throw fail(`${what} is empty`);
170
+ return items.map((item, index) => requireString(item, `${what}[${index}]`, fail));
171
+ }
172
+ /**
173
+ * A `z.json()` — a JSON value, recursively: `string | number | boolean | null`,
174
+ * an array of them, or an object of them, at ANY depth.
175
+ *
176
+ * WHY THIS EXISTS, given it can never fire over the wire. Every SDK response
177
+ * has already been through `JSON.parse`, so a `Date`, `bigint`, `undefined`, or
178
+ * function CANNOT be present by construction — this check is unreachable in
179
+ * practice. It is here for PARITY: the canonical `sourceImageAnalysisResultSchema`
180
+ * uses `z.json()` precisely because those four break differently on the way to
181
+ * `jsonb` (a `bigint` throws in `JSON.stringify`, an `undefined` is silently
182
+ * dropped, and a `Date` round-trips as an ISO *string* — a different type read
183
+ * back than written). The mirror has to reject what the schema rejects or the
184
+ * parity test is lying. Do not delete this as dead code.
185
+ */
186
+ export function requireJsonValue(value, what, fail) {
187
+ if (value === null)
188
+ return null;
189
+ if (typeof value === "string" || typeof value === "boolean")
190
+ return value;
191
+ if (typeof value === "number") {
192
+ // `z.json()`'s number leaf rejects NaN and ±Infinity, and `1e999` parses out
193
+ // of JSON as `Infinity` — so finiteness IS reachable, unlike the rest.
194
+ if (!Number.isFinite(value))
195
+ throw fail(`${what} is not a finite number`);
196
+ return value;
197
+ }
198
+ if (Array.isArray(value)) {
199
+ return value.map((item, index) => requireJsonValue(item, `${what}[${index}]`, fail));
200
+ }
201
+ if (isRecord(value)) {
202
+ // A PLAIN object only. `isRecord` alone is not enough: it answers true for a
203
+ // `Date`, a `Map`, and any class instance, none of which have own enumerable
204
+ // entries — so recursing over them would silently accept them as `{}` while
205
+ // `z.json()` rejects them outright. A `Date` reaching `jsonb` is exactly the
206
+ // failure this check exists to stop.
207
+ const prototype = Object.getPrototypeOf(value);
208
+ if (prototype !== Object.prototype && prototype !== null) {
209
+ throw fail(`${what} is not a JSON value`);
210
+ }
211
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
212
+ key,
213
+ requireJsonValue(item, `${what}.${key}`, fail),
214
+ ]));
215
+ }
216
+ throw fail(`${what} is not a JSON value`);
217
+ }
218
+ /**
219
+ * A `z.record(z.string(), z.json())` — a PLAIN JSON object of JSON values.
220
+ *
221
+ * Deliberately NOT `requireRecord` + walk the entries. That combination has a
222
+ * top-level hole: `Object.entries(new Date())` and `Object.entries(new Map())`
223
+ * are both `[]`, so there is nothing to recurse over and the value is accepted
224
+ * as `{}` — while `z.record(z.string(), z.json())` rejects both outright. A
225
+ * "non-array object" guard would close those two and still miss a class instance
226
+ * with real own properties, which canonical also rejects. Only a prototype test
227
+ * matches, and {@link requireJsonValue} already applies it at every depth — so
228
+ * this delegates there and then narrows to an object, rather than reimplementing
229
+ * the test and acquiring the same hole one level up.
230
+ *
231
+ * `requireRecord` is left alone: its other callers mirror `z.object({…})`, which
232
+ * genuinely does accept any record, and tightening a shared helper under them
233
+ * would be a change none of them asked for.
234
+ */
235
+ export function requireJsonRecord(value, what, fail) {
236
+ const json = requireJsonValue(value, what, fail);
237
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
238
+ throw fail(`${what} is not a JSON object`);
239
+ }
240
+ return json;
241
+ }