@nodaro/shared 1.7.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -3,11 +3,13 @@ import {
3
3
  LLM_MODEL_IDS,
4
4
  LLM_FEATURE_DEFAULTS,
5
5
  STRUCTURED_VISION_MODELS,
6
+ LLM_REASONING_EFFORTS,
6
7
  getLlmModel,
7
8
  getLlmTier,
8
9
  buildLlmCreditIdentifier,
9
10
  resolveLlmCreditId,
10
11
  motionGraphicsFeature,
12
+ effectiveReasoningEffort,
11
13
  } from "../llm-models.js"
12
14
  import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
13
15
 
@@ -21,8 +23,8 @@ import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
21
23
  // LLM_MODELS data integrity
22
24
  // ---------------------------------------------------------------------------
23
25
  describe("LLM_MODELS data integrity", () => {
24
- it("should have exactly 7 models", () => {
25
- expect(LLM_MODELS).toHaveLength(7)
26
+ it("should have exactly 13 models", () => {
27
+ expect(LLM_MODELS).toHaveLength(13)
26
28
  })
27
29
 
28
30
  it("each model has all required fields", () => {
@@ -57,14 +59,14 @@ describe("LLM_MODELS data integrity", () => {
57
59
  expect(new Set(ids).size).toBe(ids.length)
58
60
  })
59
61
 
60
- it("has 2 economy, 2 standard, 3 premium models", () => {
62
+ it("has 3 economy, 4 standard, 6 premium models", () => {
61
63
  const tierCounts: Record<LlmTier, number> = { economy: 0, standard: 0, premium: 0 }
62
64
  for (const model of LLM_MODELS) {
63
65
  tierCounts[model.tier]++
64
66
  }
65
- expect(tierCounts.economy).toBe(2)
66
- expect(tierCounts.standard).toBe(2)
67
- expect(tierCounts.premium).toBe(3)
67
+ expect(tierCounts.economy).toBe(3)
68
+ expect(tierCounts.standard).toBe(4)
69
+ expect(tierCounts.premium).toBe(6)
68
70
  })
69
71
 
70
72
  it("all three kieFormats are represented", () => {
@@ -111,6 +113,12 @@ describe("LLM_MODEL_IDS", () => {
111
113
  "gemini-3.1-pro",
112
114
  "claude-opus-4.7",
113
115
  "gpt-5.4",
116
+ "gpt-5.5",
117
+ "gpt-5.6-luna",
118
+ "gpt-5.6-terra",
119
+ "gpt-5.6-sol",
120
+ "claude-sonnet-5",
121
+ "claude-opus-4.8",
114
122
  ]
115
123
  expect(LLM_MODEL_IDS).toEqual(expected)
116
124
  })
@@ -420,6 +428,8 @@ describe("STRUCTURED_VISION_MODELS", () => {
420
428
  "claude-sonnet-4.6",
421
429
  "gemini-3-flash",
422
430
  "gemini-3.1-pro",
431
+ "claude-sonnet-5",
432
+ "claude-opus-4.8",
423
433
  ].sort(),
424
434
  )
425
435
  })
@@ -443,3 +453,78 @@ describe("STRUCTURED_VISION_MODELS", () => {
443
453
  }
444
454
  })
445
455
  })
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Reasoning effort registry (GPT-5.6 / Claude Sonnet 5 / Claude Opus 4.8)
459
+ // grok-4.5 is DEFERRED — its chat endpoint is not live on the provider yet
460
+ // (2026-07-13); its tests are intentionally omitted here.
461
+ // ---------------------------------------------------------------------------
462
+ describe("reasoning effort registry", () => {
463
+ it("every reasoningEfforts list is a subset of the superset, in ascending order", () => {
464
+ const rank = Object.fromEntries(LLM_REASONING_EFFORTS.map((e, i) => [e, i]))
465
+ for (const m of LLM_MODELS) {
466
+ for (const e of m.reasoningEfforts ?? []) expect(LLM_REASONING_EFFORTS).toContain(e)
467
+ const ranks = (m.reasoningEfforts ?? []).map((e) => rank[e])
468
+ expect([...ranks].sort((a, b) => a - b)).toEqual(ranks)
469
+ }
470
+ })
471
+ it("new models exist with expected tiers", () => {
472
+ expect(getLlmTier("gpt-5.6-luna")).toBe("economy")
473
+ expect(getLlmTier("gpt-5.6-terra")).toBe("standard")
474
+ expect(getLlmTier("gpt-5.6-sol")).toBe("premium")
475
+ expect(getLlmTier("claude-sonnet-5")).toBe("standard")
476
+ expect(getLlmTier("claude-opus-4.8")).toBe("premium")
477
+ expect(getLlmTier("gpt-5.5")).toBe("premium")
478
+ })
479
+ })
480
+
481
+ describe("effectiveReasoningEffort", () => {
482
+ it("passes through a supported level", () => {
483
+ expect(effectiveReasoningEffort("claude-sonnet-5", "max")).toBe("max")
484
+ })
485
+ it("clamps down to the highest supported level ≤ requested", () => {
486
+ expect(effectiveReasoningEffort("gpt-5.4", "xhigh")).toBe("high")
487
+ })
488
+ it("returns undefined when the model has no levels", () => {
489
+ expect(effectiveReasoningEffort("gemini-3-flash", "high")).toBeUndefined()
490
+ })
491
+ it("returns undefined for none on Claude (below its lowest level)", () => {
492
+ expect(effectiveReasoningEffort("claude-sonnet-5", "none")).toBeUndefined()
493
+ })
494
+ it("returns undefined for undefined/garbage input", () => {
495
+ expect(effectiveReasoningEffort("claude-sonnet-5", undefined)).toBeUndefined()
496
+ expect(effectiveReasoningEffort("claude-sonnet-5", "turbo")).toBeUndefined()
497
+ })
498
+ })
499
+
500
+ describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
501
+ it("economy + max → standard (bare feature)", () => {
502
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-luna", "max")).toBe("llm-chat")
503
+ })
504
+ it("standard + xhigh → premium", () => {
505
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-terra", "xhigh")).toBe("llm-chat:premium")
506
+ })
507
+ it("premium + max stays premium", () => {
508
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-sol", "max")).toBe("llm-chat:premium")
509
+ })
510
+ it("high never bumps", () => {
511
+ expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-5", "high")).toBe("llm-chat")
512
+ })
513
+ it("clamp on a partial-list standard model never bumps (sonnet-4.6 @ xhigh → high)", () => {
514
+ expect(buildLlmCreditIdentifier("llm-chat", "claude-sonnet-4.6", "xhigh")).toBe("llm-chat")
515
+ })
516
+ it("bump uses the CLAMPED effort (xhigh on a low/medium/high model clamps to high → no bump)", () => {
517
+ expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.4", "xhigh")).toBe("llm-chat:premium")
518
+ // gpt-5.4 is premium anyway; the real clamp case:
519
+ expect(buildLlmCreditIdentifier("llm-chat", "gemini-3-flash", "max")).toBe("llm-chat:economy")
520
+ })
521
+ it("back-compat: no effort arg → identical to today for every model", () => {
522
+ for (const m of LLM_MODELS) {
523
+ const before = m.tier === "standard" ? "x" : `x:${m.tier}`
524
+ expect(buildLlmCreditIdentifier("x", m.id)).toBe(before)
525
+ }
526
+ })
527
+ it("resolveLlmCreditId reads reasoningEffort from the raw body", () => {
528
+ expect(resolveLlmCreditId("llm-chat", { llmModel: "gpt-5.6-terra", reasoningEffort: "max" })).toBe("llm-chat:premium")
529
+ })
530
+ })
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ export {
92
92
  SEED_SUPPORT,
93
93
  RENDERING_SPEED_SUPPORT,
94
94
  GUIDANCE_SCALE_SUPPORT,
95
+ SEEDANCE_2_PROVIDERS,
95
96
  SEEDANCE_2_REF_LIMITS,
96
97
  SEEDANCE_2_EXTEND_STITCH,
97
98
  NATIVE_ADAPTIVE_ASPECT,
@@ -251,16 +252,20 @@ export {
251
252
  VIDEO_ANALYSIS_LLM_MODELS,
252
253
  LLM_FEATURE_DEFAULTS,
253
254
  LLM_MODALITY_CAPS,
255
+ LLM_REASONING_EFFORTS,
256
+ EFFORT_TIER_BUMP,
254
257
  getLlmModel,
255
258
  getLlmTier,
256
259
  getLlmModalityCaps,
257
260
  buildLlmCreditIdentifier,
258
261
  resolveLlmCreditId,
259
262
  motionGraphicsFeature,
263
+ effectiveReasoningEffort,
260
264
  type LlmTier,
261
265
  type LlmFeature,
262
266
  type KieApiFormat,
263
267
  type LlmModelDef,
268
+ type LlmReasoningEffort,
264
269
  } from "./llm-models.js"
265
270
 
266
271
  export {
package/src/llm-models.ts CHANGED
@@ -16,6 +16,12 @@
16
16
  export type LlmTier = "economy" | "standard" | "premium"
17
17
  export type KieApiFormat = "chat-completions" | "messages" | "responses"
18
18
 
19
+ export const LLM_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] as const
20
+ export type LlmReasoningEffort = (typeof LLM_REASONING_EFFORTS)[number]
21
+ /** Levels that bill one tier up. `high` is the Claude-family server default — it never bumps. */
22
+ export const EFFORT_TIER_BUMP: ReadonlySet<LlmReasoningEffort> = new Set(["xhigh", "max"])
23
+ const EFFORT_RANK: Record<LlmReasoningEffort, number> = { none: 0, low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }
24
+
19
25
  export interface LlmModelDef {
20
26
  id: string
21
27
  displayName: string
@@ -41,6 +47,12 @@ export interface LlmModelDef {
41
47
  structuredOutputMode?: "anthropic-tool" | "kie-response-format"
42
48
  /** If set, fallback to direct Anthropic SDK with this model ID when KIE.ai fails */
43
49
  directFallbackModel?: string
50
+ /** Effort levels this model accepts (ascending). Absent/empty = no effort lever, picker hidden. */
51
+ reasoningEfforts?: readonly LlmReasoningEffort[]
52
+ /** false = model rejects `temperature` (Claude 5-era, GPT-5.6). Absent = accepts. */
53
+ supportsTemperature?: false
54
+ /** Claude-only: KIE is the preferred routing, direct Anthropic the fallback. */
55
+ preferKie?: true
44
56
  }
45
57
 
46
58
  export const LLM_MODELS: readonly LlmModelDef[] = [
@@ -81,6 +93,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
81
93
  supportsImages: true,
82
94
  maxOutputTokens: 16384,
83
95
  directFallbackModel: "claude-sonnet-4-6",
96
+ reasoningEfforts: ["low", "medium", "high", "max"],
84
97
  },
85
98
  {
86
99
  id: "gpt-5.2",
@@ -117,6 +130,9 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
117
130
  supportsImages: true,
118
131
  maxOutputTokens: 16384,
119
132
  directFallbackModel: "claude-opus-4-7",
133
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
134
+ supportsTemperature: false,
135
+ preferKie: true,
120
136
  },
121
137
  {
122
138
  id: "gpt-5.4",
@@ -128,6 +144,92 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
128
144
  vendor: "openai",
129
145
  supportsImages: true,
130
146
  maxOutputTokens: 16384,
147
+ reasoningEfforts: ["low", "medium", "high"],
148
+ },
149
+ {
150
+ id: "gpt-5.5",
151
+ displayName: "GPT-5.5",
152
+ desc: "Previous flagship GPT, deep reasoning",
153
+ tier: "premium",
154
+ kieFormat: "responses",
155
+ kieSlugOrModel: "gpt-5-5",
156
+ vendor: "openai",
157
+ supportsImages: true,
158
+ maxOutputTokens: 16384,
159
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
160
+ supportsTemperature: false,
161
+ },
162
+ {
163
+ id: "gpt-5.6-luna",
164
+ displayName: "GPT-5.6 Luna",
165
+ desc: "Fastest GPT-5.6, high-volume workloads",
166
+ tier: "economy",
167
+ kieFormat: "responses",
168
+ kieSlugOrModel: "gpt-5-6-luna",
169
+ vendor: "openai",
170
+ supportsImages: true,
171
+ maxOutputTokens: 16384,
172
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
173
+ supportsTemperature: false,
174
+ },
175
+ {
176
+ id: "gpt-5.6-terra",
177
+ displayName: "GPT-5.6 Terra",
178
+ desc: "Balanced GPT-5.6 for production work",
179
+ tier: "standard",
180
+ kieFormat: "responses",
181
+ kieSlugOrModel: "gpt-5-6-terra",
182
+ vendor: "openai",
183
+ supportsImages: true,
184
+ maxOutputTokens: 16384,
185
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
186
+ supportsTemperature: false,
187
+ },
188
+ {
189
+ id: "gpt-5.6-sol",
190
+ displayName: "GPT-5.6 Sol",
191
+ desc: "Flagship GPT-5.6, deepest reasoning",
192
+ tier: "premium",
193
+ kieFormat: "responses",
194
+ kieSlugOrModel: "gpt-5-6-sol",
195
+ vendor: "openai",
196
+ supportsImages: true,
197
+ maxOutputTokens: 16384,
198
+ reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
199
+ supportsTemperature: false,
200
+ },
201
+ // grok-4.5 deferred — KIE chat endpoint not yet live (2026-07-13); add entry + rate row + docs when it activates.
202
+ {
203
+ id: "claude-sonnet-5",
204
+ displayName: "Claude Sonnet 5",
205
+ desc: "Near-Opus quality at Sonnet cost",
206
+ tier: "standard",
207
+ kieFormat: "messages",
208
+ kieSlugOrModel: "claude-sonnet-5",
209
+ vendor: "anthropic",
210
+ structuredOutputMode: "anthropic-tool",
211
+ supportsImages: true,
212
+ maxOutputTokens: 16384,
213
+ directFallbackModel: "claude-sonnet-5",
214
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
215
+ supportsTemperature: false,
216
+ preferKie: true,
217
+ },
218
+ {
219
+ id: "claude-opus-4.8",
220
+ displayName: "Claude Opus 4.8",
221
+ desc: "Most capable Claude, long-horizon work",
222
+ tier: "premium",
223
+ kieFormat: "messages",
224
+ kieSlugOrModel: "claude-opus-4-8",
225
+ vendor: "anthropic",
226
+ structuredOutputMode: "anthropic-tool",
227
+ supportsImages: true,
228
+ maxOutputTokens: 16384,
229
+ directFallbackModel: "claude-opus-4-8",
230
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
231
+ supportsTemperature: false,
232
+ preferKie: true,
131
233
  },
132
234
  ] as const
133
235
 
@@ -201,6 +303,12 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
201
303
  "claude-opus-4.7": { image: true, video: false, audio: false },
202
304
  "gpt-5.2": { image: true, video: false, audio: false },
203
305
  "gpt-5.4": { image: true, video: false, audio: false },
306
+ "gpt-5.5": { image: true, video: false, audio: false },
307
+ "gpt-5.6-luna": { image: true, video: false, audio: false },
308
+ "gpt-5.6-terra": { image: true, video: false, audio: false },
309
+ "gpt-5.6-sol": { image: true, video: false, audio: false },
310
+ "claude-sonnet-5": { image: true, video: false, audio: false },
311
+ "claude-opus-4.8": { image: true, video: false, audio: false },
204
312
  }
205
313
 
206
314
  /** Capability lookup with safe default — unknown models get image-only. */
@@ -217,26 +325,51 @@ export function getLlmTier(id: string): LlmTier {
217
325
  return getLlmModel(id)?.tier ?? "standard"
218
326
  }
219
327
 
328
+ /** Highest level the model supports that is ≤ the requested level; undefined = treat as Auto. */
329
+ export function effectiveReasoningEffort(
330
+ modelId: string | undefined,
331
+ requested?: string,
332
+ ): LlmReasoningEffort | undefined {
333
+ if (!requested || !(requested in EFFORT_RANK)) return undefined
334
+ const levels = getLlmModel(modelId ?? "")?.reasoningEfforts
335
+ if (!levels || levels.length === 0) return undefined
336
+ const req = requested as LlmReasoningEffort
337
+ let best: LlmReasoningEffort | undefined
338
+ for (const l of levels) {
339
+ if (EFFORT_RANK[l] <= EFFORT_RANK[req] && (best === undefined || EFFORT_RANK[l] > EFFORT_RANK[best])) best = l
340
+ }
341
+ return best
342
+ }
343
+
220
344
  /**
221
345
  * Build a composite credit identifier for an LLM feature.
222
346
  * - economy tier → "ai-writer:economy"
223
347
  * - standard tier → "ai-writer" (no suffix — backward compatible)
224
348
  * - premium tier → "ai-writer:premium"
349
+ * A reasoning effort of "xhigh" or "max" (after clamping to what the model
350
+ * actually supports) bills one tier up (economy→standard, standard→premium;
351
+ * premium stays premium). `high` is the Claude-family server default and
352
+ * never bumps.
225
353
  */
226
- export function buildLlmCreditIdentifier(feature: string, modelId?: string): string {
354
+ export function buildLlmCreditIdentifier(feature: string, modelId?: string, reasoningEffort?: string): string {
227
355
  if (!modelId) return feature
228
- const tier = getLlmTier(modelId)
356
+ let tier = getLlmTier(modelId)
357
+ const eff = effectiveReasoningEffort(modelId, reasoningEffort)
358
+ if (eff !== undefined && EFFORT_TIER_BUMP.has(eff)) {
359
+ if (tier === "economy") tier = "standard"
360
+ else if (tier === "standard") tier = "premium"
361
+ }
229
362
  if (tier === "standard") return feature
230
363
  return `${feature}:${tier}`
231
364
  }
232
365
 
233
366
  /**
234
- * Resolve llmModel from raw body for creditGuard preHandler (before Zod parsing).
235
- * Returns the credit identifier for the given feature + optional model.
367
+ * Resolve llmModel (+ reasoningEffort) from raw body for creditGuard preHandler
368
+ * (before Zod parsing). Returns the credit identifier for the given feature.
236
369
  */
237
370
  export function resolveLlmCreditId(feature: string, body: unknown): string {
238
- const llmModel = (body as Record<string, unknown>)?.llmModel as string | undefined
239
- return buildLlmCreditIdentifier(feature, llmModel)
371
+ const b = body as Record<string, unknown> | undefined
372
+ return buildLlmCreditIdentifier(feature, b?.llmModel as string | undefined, b?.reasoningEffort as string | undefined)
240
373
  }
241
374
 
242
375
  /** Models capable of video-analysis: capability-derived, never hand-listed (route-enum-sync convention). */
@@ -1139,12 +1139,18 @@ export const SEEDANCE_2_REF_LIMITS = {
1139
1139
  * boundary clicks without shifting sync (combineVideos cut+crossfade path).
1140
1140
  */
1141
1141
  export const SEEDANCE_2_EXTEND_STITCH = {
1142
- /** Frames dropped from the END of the source clip. */
1142
+ /** Frames dropped from the END of the source clip (smart-cut FALLBACK —
1143
+ * used only when the PSNR boundary matcher finds no genuine match). */
1143
1144
  trimTailFrames: 4,
1144
- /** Frames dropped from the START of the generated extension. */
1145
+ /** Frames dropped from the START of the generated extension (smart-cut
1146
+ * fallback, see above). */
1145
1147
  trimHeadFrames: 3,
1146
1148
  /** Boundary audio fade length (seconds), timeline-preserving. */
1147
1149
  audioFadeSec: 0.15,
1150
+ /** Seconds of the source's TAIL passed as the @video_1 reference —
1151
+ * spike-validated: a short tail keeps the model focused on continuing
1152
+ * the boundary motion instead of re-staging the whole clip. */
1153
+ referenceTailSeconds: 1,
1148
1154
  } as const
1149
1155
 
1150
1156
  /**
@@ -15,6 +15,12 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
15
15
  // for back-compat with un-migrated workflow JSON). Mirrors text-to-video so
16
16
  // fieldMappings/{} injection AND missing-ref detection work on the live node.
17
17
  "generate-video": ["prompt", "negativePrompt"],
18
+ // Trimmed multi-segment stitch variant of generate-video — prompt only, no
19
+ // negativePrompt field on the node.
20
+ "generate-video-pro": ["prompt"],
21
+ // Span-replace sibling of generate-video-pro — prompt only, no
22
+ // negativePrompt field on the node.
23
+ "edit-video-pro": ["prompt"],
18
24
  "video-analysis": ["analysisFocus", "youtubeUrl"],
19
25
  "video-to-video": ["prompt"],
20
26
  "text-to-speech": ["directText"],
@@ -30,6 +30,16 @@ export const VIDEO_PRODUCER_TYPES: ReadonlySet<string> = new Set([
30
30
  // through to the imageUrl/videoUrl/audioUrl/text default and downstream consumers
31
31
  // could silently misroute the output.
32
32
  "generate-video",
33
+ // Generate Video Pro — Seedance-2-family multi-segment stitch variant of
34
+ // generate-video (same "emits videoUrl" contract; a trimmed provider +
35
+ // handle set). Must mirror generate-video here or its output can't connect
36
+ // downstream (the recurring "cannot connect the outputs" bug class).
37
+ "generate-video-pro",
38
+ // Edit Video Pro — Seedance-2-family span-replace sibling of generate-
39
+ // video-pro (same "emits videoUrl" contract; source video + prompt in,
40
+ // ONE video out). Must mirror generate-video-pro here or its output can't
41
+ // connect downstream (the recurring "cannot connect the outputs" bug class).
42
+ "edit-video-pro",
33
43
  "upload-video",
34
44
  "youtube-video",
35
45
  "combine-videos",
package/src/selector.ts CHANGED
@@ -706,7 +706,16 @@ export function applyRangeIndices(
706
706
  return result
707
707
  }
708
708
 
709
- /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer). */
709
+ /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer).
710
+ *
711
+ * TWIN WARNING: backend/src/providers/video/audio-fx.ts carries its own
712
+ * private mulberry32 (cosmetically different — `>>> 0` seed masking — but the
713
+ * same sequence). The two are deliberately INDEPENDENT and both frozen: the
714
+ * audio-fx copy seeds every reverb impulse response that the committed
715
+ * characterization goldens (and every customer's rendered reverb waveform)
716
+ * are pinned to. Do NOT "deduplicate" them into one shared export or "sync"
717
+ * one to match the other — a change to either sequence is a silent behavior
718
+ * change in its domain. */
710
719
  function mulberry32(seed: number): () => number {
711
720
  let a = seed | 0
712
721
  return () => {