@ak--47/dungeon-master 1.6.0 → 1.6.2

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 (36) hide show
  1. package/.claude/skills/powertools/SKILL.md +75 -0
  2. package/.claude/skills/powertools/pt.mjs +91 -0
  3. package/.claude/skills/powertools/snapshot-project.mjs +124 -0
  4. package/CHANGELOG.md +138 -0
  5. package/README.md +2 -0
  6. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +4 -5
  7. package/dungeons/vertical/community/community.verify.mjs +4 -6
  8. package/dungeons/vertical/crypto/crypto.verify.mjs +4 -2
  9. package/dungeons/vertical/dating/dating.verify.mjs +4 -6
  10. package/dungeons/vertical/devtools/devtools.verify.mjs +4 -2
  11. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +4 -4
  12. package/dungeons/vertical/education/education.verify.mjs +4 -9
  13. package/dungeons/vertical/fintech/fintech.verify.mjs +4 -4
  14. package/dungeons/vertical/fitness/fitness.verify.mjs +4 -5
  15. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +4 -2
  16. package/dungeons/vertical/gaming/gaming.verify.mjs +4 -4
  17. package/dungeons/vertical/healthcare/healthcare.verify.mjs +4 -6
  18. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +4 -2
  19. package/dungeons/vertical/logistics/logistics.verify.mjs +4 -7
  20. package/dungeons/vertical/marketplace/marketplace.verify.mjs +4 -2
  21. package/dungeons/vertical/media/media.verify.mjs +4 -2
  22. package/dungeons/vertical/real-estate/real-estate.verify.mjs +4 -9
  23. package/dungeons/vertical/sass/sass.verify.mjs +4 -2
  24. package/dungeons/vertical/social/social.verify.mjs +4 -2
  25. package/dungeons/vertical/streaming/streaming.verify.mjs +4 -2
  26. package/dungeons/vertical/support-desk/support-desk.verify.mjs +4 -2
  27. package/dungeons/vertical/travel/travel.verify.mjs +4 -6
  28. package/index.js +35 -2
  29. package/lib/core/config-validator.js +19 -1
  30. package/lib/orchestrators/mixpanel-sender.js +52 -2
  31. package/lib/utils/utils.js +146 -55
  32. package/lib/verify/index.js +6 -0
  33. package/lib/verify/verify-dungeon.js +39 -12
  34. package/package.json +4 -4
  35. package/scripts/verify-stories.mjs +2 -1
  36. package/types.d.ts +17 -0
@@ -36,9 +36,35 @@ let globalUserChance;
36
36
  let userChanceInitialized = false;
37
37
 
38
38
  // Module-scoped memoization cache for weighted-array resolvers in `choose()`.
39
- // Lives for the lifetime of the node process; key is the function source string.
39
+ // Key is the function source string; cleared per run by resetValueCaches().
40
+ // Functions tagged `noCache = true` (e.g. pickAWinner closures, whose source
41
+ // strings are identical across instances) bypass this cache entirely.
40
42
  const weightedArrayCache = new Map();
41
43
 
44
+ // v1.6.1: per-run winner memo for unweighted string arrays. Two layers:
45
+ // a WeakMap keyed by array IDENTITY holding the full {idx, weights} entry
46
+ // (property arrays are stable references across a run, so the steady state is
47
+ // a single WeakMap hit per event — no join, no weight rebuild), plus a Map
48
+ // keyed by array CONTENTS so distinct array instances with equal contents
49
+ // share one winner. Guarantees ONE stable winner per array per run so
50
+ // aggregate distributions stay visibly skewed instead of cancelling to
51
+ // uniform. Cleared by resetValueCaches() at run start / initChance so winners
52
+ // never leak across runs or seeds.
53
+ let winnerEntryCache = new WeakMap();
54
+ const winnerCache = new Map();
55
+
56
+ /**
57
+ * Clear the per-run value-resolution caches (winner memo + weighted-array
58
+ * resolver cache). Called by initChance() and at the top of every runDungeon
59
+ * so in-process back-to-back runs (different seeds, different users) never
60
+ * inherit a prior run's winners.
61
+ */
62
+ function resetValueCaches() {
63
+ winnerEntryCache = new WeakMap();
64
+ winnerCache.clear();
65
+ weightedArrayCache.clear();
66
+ }
67
+
42
68
  // v1.5.1: dataset-window state moved to AsyncLocalStorage scope
43
69
  // (`lib/utils/dataset-context.js`). Each `generate()` call wraps the pipeline
44
70
  // in `runWithDataset(begin, now, fn)`, and factory thunks read via the
@@ -146,6 +172,7 @@ function initChance(seed) {
146
172
  if (!seed && process.env.SEED) seed = process.env.SEED;
147
173
  globalChance = new Chance(seed);
148
174
  chanceInitialized = true;
175
+ resetValueCaches();
149
176
  return globalChance;
150
177
  }
151
178
 
@@ -387,10 +414,10 @@ function choose(value) {
387
414
  // check to make sure that each element in the array only occurs once...
388
415
  const uniqueItems = new Set(value);
389
416
  if (uniqueItems.size === value.length) {
390
- // Array has no duplicates, use pickAWinner
391
- const quickList = pickAWinner(value, 0)();
392
- const theChosenOne = chance.pickone(quickList);
393
- return theChosenOne;
417
+ // Array has no duplicates power-law draw with ONE stable
418
+ // seed-deterministic winner per array per run (v1.6.1)
419
+ const entry = getWinnerEntry(/** @type {string[]} */ (value));
420
+ return chance.weighted(value, entry.weights);
394
421
  }
395
422
 
396
423
  }
@@ -408,8 +435,17 @@ function choose(value) {
408
435
  }
409
436
 
410
437
  try {
411
- // Keep resolving the value if it's a function (with caching)
438
+ // Keep resolving the value if it's a function (with caching).
439
+ // Functions tagged noCache (pickAWinner closures) skip the source-string
440
+ // cache: their toString() is identical across instances, so caching by
441
+ // source would hand one property's expansion to every other property.
412
442
  while (typeof value === 'function') {
443
+ if (/** @type {any} */ (value).noCache === true) {
444
+ const result = value();
445
+ if (result instanceof ListValue) return result;
446
+ value = result;
447
+ continue;
448
+ }
413
449
  const funcString = value.toString();
414
450
 
415
451
  if (weightedArrayCache.has(funcString)) {
@@ -452,11 +488,6 @@ function choose(value) {
452
488
  }
453
489
  }
454
490
 
455
- // ["","",""] should pick-a-winner
456
- if (Array.isArray(value) && typeof value[0] === "string") {
457
- value = pickAWinner(value)();
458
- }
459
-
460
491
  // [0,1,2] should pick one
461
492
  if (Array.isArray(value) && typeof value[0] === "number") {
462
493
  return chance.pickone(value);
@@ -988,63 +1019,122 @@ function weighChoices(items) {
988
1019
  };
989
1020
  }
990
1021
 
1022
+ /**
1023
+ * Resolve the stable per-run winner index for an array of items. Memoized in
1024
+ * `winnerCache` keyed by array contents (raw joined string — no hashing, so
1025
+ * no collision risk), so every event drawing from the same array favors the
1026
+ * SAME winner for the whole run. Winner is rolled from the seeded chance →
1027
+ * same seed = same winner; resetValueCaches() clears the memo between runs.
1028
+ *
1029
+ * @param {Array} items - The list of items to pick a winner from.
1030
+ * @returns {number} - The stable winner index for this run.
1031
+ */
1032
+ function getStableWinnerIndex(items) {
1033
+ const key = items.join('\u0000');
1034
+ if (winnerCache.has(key)) return winnerCache.get(key);
1035
+ const chance = getChance();
1036
+ const winner = chance.integer({ min: 0, max: items.length - 1 });
1037
+ winnerCache.set(key, winner);
1038
+ return winner;
1039
+ }
1040
+
1041
+ /**
1042
+ * Full winner entry ({idx, weights}) for an array, memoized by array identity
1043
+ * in a WeakMap. Steady state for the choose() hot path is one WeakMap hit per
1044
+ * event — no join, no weight rebuild. Distinct array instances with equal
1045
+ * contents converge on the same winner via getStableWinnerIndex.
1046
+ *
1047
+ * @param {string[]} items
1048
+ * @returns {{idx: number, weights: number[]}}
1049
+ */
1050
+ function getWinnerEntry(items) {
1051
+ let entry = winnerEntryCache.get(items);
1052
+ if (entry) return entry;
1053
+ const idx = getStableWinnerIndex(items);
1054
+ entry = { idx, weights: winnerWeights(items.length, idx) };
1055
+ winnerEntryCache.set(items, entry);
1056
+ return entry;
1057
+ }
1058
+
1059
+ /**
1060
+ * Build a power-law weight vector aligned to item indices: winner ~45%,
1061
+ * second ~25%, third ~15%, remainder split over the tail with geometric decay.
1062
+ * Rank order follows the rotational convention: second = (winner+1) % n,
1063
+ * third = (winner+2) % n. This is THE place to tune the curve.
1064
+ *
1065
+ * @param {number} n - Number of items.
1066
+ * @param {number} winnerIndex - Index of the winning item.
1067
+ * @returns {number[]} - Weights (unnormalized; chance.weighted normalizes).
1068
+ */
1069
+ function winnerWeights(n, winnerIndex) {
1070
+ if (n <= 0) return [];
1071
+ if (n === 1) return [1];
1072
+ const HEAD = [45, 25, 15];
1073
+ const TAIL_BUDGET = 15;
1074
+ const TAIL_DECAY = 0.7;
1075
+ const tailCount = Math.max(0, n - HEAD.length);
1076
+ let tailWeights = [];
1077
+ if (tailCount > 0) {
1078
+ const raw = [];
1079
+ let w = 1;
1080
+ for (let k = 0; k < tailCount; k++) { raw.push(w); w *= TAIL_DECAY; }
1081
+ const sum = raw.reduce((a, b) => a + b, 0);
1082
+ tailWeights = raw.map(r => (r / sum) * TAIL_BUDGET);
1083
+ }
1084
+ const weights = new Array(n).fill(0);
1085
+ for (let rank = 0; rank < n; rank++) {
1086
+ const idx = (winnerIndex + rank) % n;
1087
+ weights[idx] = rank < HEAD.length ? HEAD[rank] : tailWeights[rank - HEAD.length];
1088
+ }
1089
+ return weights;
1090
+ }
1091
+
991
1092
  /**
992
1093
  * Creates a function that generates a weighted list of items
993
1094
  * with a higher likelihood of picking a specified index and clear second and third place indices.
994
- *
1095
+ *
1096
+ * v1.6.1: the returned closure yields a DETERMINISTIC weighted expansion
1097
+ * (winner ~45%, second ~25%, third ~15%, geometric tail) built once — no
1098
+ * per-call sampling variance. When no index is passed, the winner is the
1099
+ * stable per-run memoized winner for the array (see getStableWinnerIndex),
1100
+ * so direct dungeon-file usage is stable and seed-deterministic too.
1101
+ *
995
1102
  * @param {Array} items - The list of items to pick from.
996
1103
  * @param {number} [mostChosenIndex] - The index of the item to be most favored.
997
1104
  * @returns {function} - A function that returns a weighted list of items.
998
1105
  */
999
1106
  function pickAWinner(items, mostChosenIndex) {
1000
- const chance = getChance();
1001
-
1002
- // Ensure mostChosenIndex is within the bounds of the items array
1003
1107
  if (!items) return () => { return ""; };
1004
1108
  if (!items.length) return () => { return ""; };
1005
- if (!mostChosenIndex) mostChosenIndex = chance.integer({ min: 0, max: items.length - 1 });
1006
- if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
1007
-
1008
- // Calculate second and third most chosen indices
1009
- const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
1010
- const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
1109
+ if (!Number.isFinite(mostChosenIndex)) {
1110
+ // undefined/null/NaN stable per-run memoized winner
1111
+ mostChosenIndex = getStableWinnerIndex(items);
1112
+ } else {
1113
+ mostChosenIndex = Math.floor(mostChosenIndex);
1114
+ if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
1115
+ if (mostChosenIndex < 0) mostChosenIndex = 0;
1116
+ }
1011
1117
 
1012
- // Return a function that generates a weighted list
1013
- return function () {
1014
- const weighted = [];
1015
- for (let i = 0; i < 10; i++) {
1016
- const rand = chance.d10(); // Random number between 1 and 10
1017
-
1018
- // 35% chance to favor the most chosen index
1019
- if (chance.bool({ likelihood: 35 })) {
1020
- // 50% chance to slightly alter the index
1021
- if (chance.bool({ likelihood: 50 })) {
1022
- weighted.push(items[mostChosenIndex]);
1023
- } else {
1024
- const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
1025
- let newIndex = mostChosenIndex + addOrSubtract;
1118
+ const weights = winnerWeights(items.length, mostChosenIndex);
1119
+ const total = weights.reduce((a, b) => a + b, 0);
1120
+ // scale slots with n so large arrays keep the ~45% winner share instead of
1121
+ // having Math.max(1, ...) floors dilute it
1122
+ const SLOTS = Math.max(20, items.length * 4);
1123
+ const expansion = [];
1124
+ items.forEach((item, i) => {
1125
+ // every item keeps at least one slot so no value becomes unreachable
1126
+ const count = Math.max(1, Math.round((weights[i] / total) * SLOTS));
1127
+ for (let j = 0; j < count; j++) expansion.push(item);
1128
+ });
1026
1129
 
1027
- // Ensure newIndex is within bounds
1028
- if (newIndex < 0) newIndex = 0;
1029
- if (newIndex >= items.length) newIndex = items.length - 1;
1030
- weighted.push(items[newIndex]);
1031
- }
1032
- }
1033
- // 25% chance to favor the second most chosen index
1034
- else if (chance.bool({ likelihood: 25 })) {
1035
- weighted.push(items[secondMostChosenIndex]);
1036
- }
1037
- // 15% chance to favor the third most chosen index
1038
- else if (chance.bool({ likelihood: 15 })) {
1039
- weighted.push(items[thirdMostChosenIndex]);
1040
- }
1041
- // Otherwise, pick a random item from the list
1042
- else {
1043
- weighted.push(chance.pickone(items));
1044
- }
1045
- }
1046
- return weighted;
1130
+ const resolver = function () {
1131
+ return expansion;
1047
1132
  };
1133
+ // all pickAWinner closures share one source string; without this tag,
1134
+ // choose()'s weightedArrayCache would cache the first closure's expansion
1135
+ // under that shared key and serve it for every other pickAWinner property
1136
+ resolver.noCache = true;
1137
+ return resolver;
1048
1138
  }
1049
1139
 
1050
1140
  function quickHash(str, seed = 0) {
@@ -1774,6 +1864,7 @@ export {
1774
1864
  getUniqueKeys,
1775
1865
  person,
1776
1866
  pickAWinner,
1867
+ resetValueCaches,
1777
1868
  quickHash,
1778
1869
  weighArray,
1779
1870
  validateEventConfig,
@@ -11,6 +11,12 @@
11
11
 
12
12
  export { emulateBreakdown } from './emulate-breakdown.js';
13
13
  export { verifyDungeon } from './verify-dungeon.js';
14
+ // v1.6.2: exposed on the verify surface so a standalone verify script — one that
15
+ // reads shards off disk instead of running the dungeon — can resolve funnel
16
+ // defaults (`conversionWindowDays`, `order`) before handing funnels to
17
+ // `evaluateStories` / `applyFunnelDefaults`. Use the RETURN value; it does not
18
+ // enrich the config you pass in.
19
+ export { validateDungeonConfig } from '../core/config-validator.js';
14
20
  export { deriveExpectedSchema, validateSchema } from './schema-validator.js';
15
21
  export {
16
22
  evaluateFunnel,
@@ -59,9 +59,10 @@ function findMatchingFunnel(funnels, steps) {
59
59
  * args object — the input is not mutated. Extracted from `verifyDungeon` in
60
60
  * v1.6 so the story runner (P3.3) reuses the exact same threading.
61
61
  * @param {Object} breakdownArgs - Args destined for `emulateBreakdown`.
62
- * @param {Array<Object>} funnels - VALIDATED dungeon funnels (`validateDungeonConfig`
63
- * mutates funnels in place, so post-run `config.funnels` carries resolved
64
- * `conversionWindowDays` / `order`).
62
+ * @param {Array<Object>} funnels - VALIDATED dungeon funnels — i.e. the funnels off
63
+ * `validateDungeonConfig`'s RETURN value (post-1.6.2 it no longer enriches the
64
+ * caller's object), which carry resolved `conversionWindowDays` / `order`. A run
65
+ * result exposes them as `result.validatedConfig.funnels`.
65
66
  * @param {Array<Object>} [profiles] - User profiles, threaded into `timeToConvert`.
66
67
  * @returns {Object}
67
68
  */
@@ -113,20 +114,44 @@ export function applyFunnelDefaults(breakdownArgs, funnels, profiles) {
113
114
  /**
114
115
  * @param {Object} config - Dungeon config (or path; passed straight to DUNGEON_MASTER).
115
116
  * @param {VerifyCheck[]} checks
116
- * @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }>, schemaReport: Object }>}
117
+ * @param {Object} [overrides] - v1.6.2: merged into the dungeon before it runs, same as
118
+ * `DUNGEON_MASTER`'s second argument. Lets CI verify a production-scale dungeon at a
119
+ * small `numUsers` / `numEvents` without editing it.
120
+ * @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }>, schemaReport: Object, validatedConfig: Object }>}
117
121
  */
118
- export async function verifyDungeon(config, checks) {
122
+ export async function verifyDungeon(config, checks, overrides) {
119
123
  if (!checks || !checks.length) throw new Error('verifyDungeon: at least one check required');
120
- let result = await DUNGEON_MASTER(config);
121
- if (Array.isArray(result)) result = result[0];
124
+ let result = await DUNGEON_MASTER(config, overrides);
125
+ if (Array.isArray(result)) {
126
+ // v1.6.2: an array input runs N dungeons but checks are written against ONE
127
+ // schema, and we used to silently verify `result[0]` and throw the rest away —
128
+ // a green report for dungeons that were never looked at. Call verifyDungeon
129
+ // per dungeon instead.
130
+ if (result.length !== 1) {
131
+ throw new Error(
132
+ `verifyDungeon: got ${result.length} dungeons, but checks apply to one. ` +
133
+ `Call verifyDungeon once per dungeon.`
134
+ );
135
+ }
136
+ result = result[0];
137
+ }
122
138
  const events = Array.isArray(result.eventData) ? result.eventData : Array.from(result.eventData);
123
139
  const profiles = Array.isArray(result.userProfilesData) ? result.userProfilesData : Array.from(result.userProfilesData);
124
- const schemaReport = validateSchema(events, config);
140
+ // v1.6.2: schema-check against the config the run actually used. `config` may be a
141
+ // PATH STRING (no fields at all → every property reads as unexpected), and even for
142
+ // an object input a v1.5.1 dungeon keeps `hasAndroidDevices` / `hasBrowser` under
143
+ // `switches`, which `deriveExpectedSchema` only reads once flattened.
144
+ const schemaReport = validateSchema(events, result.validatedConfig || config);
125
145
  const ctx = { events, profiles, schemaReport };
126
146
  const results = [];
127
- // `validateDungeonConfig` mutates funnels in place — by the time we read here,
128
- // `conversionWindowDays` is populated and `order` is the validated value.
129
- const validatedFunnels = (config && Array.isArray(config.funnels)) ? config.funnels : [];
147
+ // v1.6.2: read the funnels off the run's `validatedConfig` `conversionWindowDays`
148
+ // and the resolved `order` live there. Before 1.6.2 we read them back off the
149
+ // caller's own `config`, relying on `validateDungeonConfig` enriching in place;
150
+ // that also meant a path/JSON input (no `.funnels` on the string) silently got an
151
+ // empty funnel list and thus an unbounded conversion window. Both are fixed here.
152
+ const validatedFunnels = Array.isArray(result?.validatedConfig?.funnels)
153
+ ? result.validatedConfig.funnels
154
+ : (config && Array.isArray(config.funnels)) ? config.funnels : [];
130
155
  for (const check of checks) {
131
156
  try {
132
157
  const breakdownArgs = applyFunnelDefaults(check.breakdown, validatedFunnels, profiles);
@@ -138,5 +163,7 @@ export async function verifyDungeon(config, checks) {
138
163
  }
139
164
  }
140
165
  const pass = results.every(r => r.pass) && schemaReport.pass;
141
- return { pass, results, schemaReport };
166
+ // v1.6.2: hand back the enriched config the run used, so callers can read
167
+ // resolved funnel/event values without re-validating.
168
+ return { pass, results, schemaReport, validatedConfig: result.validatedConfig };
142
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ak--47/dungeon-master",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "generate fancy datasets",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -33,7 +33,7 @@
33
33
  "HOOKS.md"
34
34
  ],
35
35
  "engines": {
36
- "node": ">=18.0.0"
36
+ "node": ">=20.20.0"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
@@ -87,7 +87,7 @@
87
87
  "dotenv": "^16.4.5",
88
88
  "hyparquet-writer": "^0.6.1",
89
89
  "mixpanel": "^0.18.0",
90
- "mixpanel-import": "^3.3.2",
90
+ "mixpanel-import": "^3.5.1",
91
91
  "p-limit": "^3.1.0",
92
92
  "pino": "^9.0.0",
93
93
  "pino-pretty": "^11.0.0",
@@ -108,4 +108,4 @@
108
108
  "tmp/"
109
109
  ]
110
110
  }
111
- }
111
+ }
@@ -176,7 +176,8 @@ if (inMemory) {
176
176
 
177
177
  // Funnel auto-threading reads VALIDATED funnel fields (conversionWindowDays,
178
178
  // order). The dungeon was not run in this process, so validate the config
179
- // here — validateDungeonConfig resolves those defaults in place.
179
+ // here and use the RETURN value as of v1.6.2 validateDungeonConfig does not
180
+ // enrich the object you hand it.
180
181
  const validated = validateDungeonConfig({ ...config, token: '' });
181
182
  const identityMap = buildIdentityMap(profiles);
182
183
 
package/types.d.ts CHANGED
@@ -1324,6 +1324,21 @@ export type Result = {
1324
1324
  * so `userProfilesData.length - profilesPushed` = dropped profile count.
1325
1325
  */
1326
1326
  profilesPushed?: number;
1327
+ /**
1328
+ * v1.6.2: the enriched config this run actually used. `validateDungeonConfig`
1329
+ * no longer writes back to the object you passed in, so resolved values —
1330
+ * `funnels[].conversionWindowDays`, `events[].isStrictEvent`, the resolved
1331
+ * dataset window — must be read here rather than off your own config.
1332
+ *
1333
+ * READ-ONLY. Do NOT feed this back into `DUNGEON_MASTER` — validation is not
1334
+ * idempotent (each pass appends to the funnel set, eventually producing an
1335
+ * empty `sequence`), and the object carries engine scratch fields. Re-run the
1336
+ * ORIGINAL config instead.
1337
+ *
1338
+ * Credentials (`token`, `serviceAccount`, `serviceSecret`, `projectId`,
1339
+ * `credentials`) are stripped — a Result is a thing hosts log.
1340
+ */
1341
+ validatedConfig?: Dungeon;
1327
1342
  /** Progress callback summary. Only present when `onProgress` was provided. */
1328
1343
  progress?: ProgressSummary;
1329
1344
  };
@@ -2358,6 +2373,8 @@ declare module '@ak--47/dungeon-master/utils' {
2358
2373
  export function objectList(template: Record<string, ValueValid>, options?: { min?: number; max?: number }): () => Array<Record<string, unknown>>;
2359
2374
  export function weighNumRange(min: number, max: number, skew?: number, size?: number): number[];
2360
2375
  export function pickAWinner(items: string[], mostChosenIndex?: number): () => string[];
2376
+ /** Clears the per-run stable-winner + weighted-array caches (also called by initChance and at every run start). */
2377
+ export function resetValueCaches(): void;
2361
2378
  export function initChance(seed?: string): unknown;
2362
2379
  export function initUserChance(seed?: string): unknown;
2363
2380
  export function getUserChance(): unknown;