@ak--47/dungeon-master 1.4.0 → 1.4.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.
package/index.js CHANGED
@@ -18,14 +18,14 @@ import { detectInputType, loadFromFile, loadFromText, parseJSONDungeon, validate
18
18
 
19
19
  // Orchestrators
20
20
  import { userLoop } from './lib/orchestrators/user-loop.js';
21
- import { sendToMixpanel } from './lib/orchestrators/mixpanel-sender.js';
21
+ import { sendToMixpanel, collectWrittenFiles } from './lib/orchestrators/mixpanel-sender.js';
22
22
  // Generators
23
23
  import { makeAdSpend } from './lib/generators/adspend.js';
24
24
  import { makeMirror } from './lib/generators/mirror.js';
25
25
  import { makeGroupProfile, makeProfile } from './lib/generators/profiles.js';
26
26
 
27
27
  // Utilities
28
- import { initChance, setDatasetNow } from './lib/utils/utils.js';
28
+ import { initChance, setDatasetNow, deleteFile } from './lib/utils/utils.js';
29
29
 
30
30
  // External dependencies
31
31
  import dayjs from "dayjs";
@@ -122,6 +122,7 @@ async function runDungeon(config) {
122
122
 
123
123
  if (config.verbose) logger.info({ seed: config.seed }, 'Configuring dungeon');
124
124
  let validatedConfig;
125
+ let storage;
125
126
  try {
126
127
  // Initialize seeded RNG BEFORE validation — config-validator captures a
127
128
  // chance reference for default userProps (spiritAnimal). If we init after,
@@ -148,7 +149,7 @@ async function runDungeon(config) {
148
149
 
149
150
  // Step 3: Initialize storage containers
150
151
  const storageManager = new StorageManager(context);
151
- const storage = await storageManager.initializeContainers();
152
+ storage = await storageManager.initializeContainers();
152
153
  updateContextWithStorage(context, storage);
153
154
 
154
155
  // ! DATA GENERATION STARTS HERE
@@ -215,7 +216,7 @@ async function runDungeon(config) {
215
216
  return {
216
217
  ...extractedData,
217
218
  importResults,
218
- files: await extractFileInfo(storage, validatedConfig),
219
+ files: extractFileInfo(storage),
219
220
  time: { start, end, delta, human },
220
221
  operations: context.getOperations(),
221
222
  eventCount: context.getStoredEventCount(),
@@ -225,6 +226,15 @@ async function runDungeon(config) {
225
226
  } catch (error) {
226
227
  logger.error({ err: error }, `Error: ${error.message}`);
227
228
  throw error;
229
+ } finally {
230
+ if (validatedConfig?.cleanup && storage) {
231
+ const allFiles = collectWrittenFiles(storage);
232
+ if (allFiles.length > 0) {
233
+ if (validatedConfig.verbose) console.log(`\nCleaning up ${allFiles.length} written files...`);
234
+ await Promise.allSettled(allFiles.map(f => deleteFile(f)));
235
+ if (validatedConfig.verbose) console.log(`Cleanup complete`);
236
+ }
237
+ }
228
238
  }
229
239
  }
230
240
 
@@ -480,74 +490,10 @@ async function flushStorageToDisk(storage, config) {
480
490
  /**
481
491
  * Extract file information from storage containers
482
492
  * @param {import('./types').Storage} storage - Storage object
483
- * @param {import('./types').Dungeon} config - Configuration object
484
- * @returns {Promise<string[]>} Array of file paths
493
+ * @returns {string[]} Array of file paths
485
494
  */
486
- async function extractFileInfo(storage, config) {
487
- const files = [];
488
-
489
- // Try to get paths from containers first
490
- Object.values(storage).forEach(container => {
491
- if (Array.isArray(container)) {
492
- container.forEach(subContainer => {
493
- if (subContainer?.getWritePath) {
494
- files.push(subContainer.getWritePath());
495
- }
496
- });
497
- } else if (container?.getWritePath) {
498
- files.push(container.getWritePath());
499
- }
500
- });
501
-
502
- // If no files found from containers and writeToDisk is enabled, scan the data directory
503
- if (files.length === 0 && config.writeToDisk) {
504
- try {
505
- const fs = await import('fs');
506
- const path = await import('path');
507
-
508
- let dataDir = path.resolve("./data");
509
- if (!fs.existsSync(dataDir)) {
510
- dataDir = path.resolve("./");
511
- }
512
-
513
- if (fs.existsSync(dataDir)) {
514
- const allFiles = fs.readdirSync(dataDir);
515
- const simulationName = config.name;
516
-
517
- // Filter files that match our patterns and were likely created by this run
518
- const relevantFiles = allFiles.filter(file => {
519
- // Skip system files
520
- if (file.startsWith('.')) return false;
521
-
522
- // If we have a simulation name, only include files with that prefix
523
- if (simulationName && !file.startsWith(simulationName)) {
524
- return false;
525
- }
526
-
527
- // Check for common patterns
528
- const hasEventPattern = file.includes('-EVENTS.');
529
- const hasUserPattern = file.includes('-USERS.');
530
- const hasScdPattern = file.includes('-SCD.');
531
- const hasGroupPattern = file.includes('-GROUPS.');
532
- const hasLookupPattern = file.includes('-LOOKUP.');
533
- const hasAdspendPattern = file.includes('-ADSPEND.');
534
- const hasMirrorPattern = file.includes('-MIRROR.');
535
-
536
- return hasEventPattern || hasUserPattern || hasScdPattern ||
537
- hasGroupPattern || hasLookupPattern || hasAdspendPattern || hasMirrorPattern;
538
- });
539
-
540
- // Convert to full paths
541
- relevantFiles.forEach(file => {
542
- files.push(path.join(dataDir, file));
543
- });
544
- }
545
- } catch (error) {
546
- // If scanning fails, just return empty array
547
- }
548
- }
549
-
550
- return files;
495
+ function extractFileInfo(storage) {
496
+ return collectWrittenFiles(storage);
551
497
  }
552
498
 
553
499
  /**
@@ -30,7 +30,7 @@ import { resolveMacro } from "../templates/macro-presets.js";
30
30
  * @param {number} [userNumDays]
31
31
  * @returns {{ datasetStartUnix: number, datasetEndUnix: number, numDays: number }}
32
32
  */
33
- function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
33
+ function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays, verbose = false) {
34
34
  const hasStart = datasetStart !== undefined && datasetStart !== null;
35
35
  const hasEnd = datasetEnd !== undefined && datasetEnd !== null;
36
36
 
@@ -48,7 +48,7 @@ function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
48
48
  throw new Error(`datasetEnd (${datasetEnd}) must be after datasetStart (${datasetStart}).`);
49
49
  }
50
50
  const derivedNumDays = Math.max(1, Math.round((endUnix - startUnix) / 86400));
51
- if (userNumDays !== undefined && userNumDays !== null && userNumDays !== derivedNumDays) {
51
+ if (verbose && userNumDays !== undefined && userNumDays !== null && userNumDays !== derivedNumDays) {
52
52
  console.warn(
53
53
  `⚠️ datasetStart/datasetEnd take precedence; user-supplied numDays=${userNumDays} ignored, derived numDays=${derivedNumDays}.`
54
54
  );
@@ -60,7 +60,7 @@ function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
60
60
  const fallbackNumDays = (typeof userNumDays === 'number' && userNumDays > 0) ? userNumDays : 30;
61
61
  const todayStart = dayjs().startOf('day').unix();
62
62
  const fallbackStart = todayStart - fallbackNumDays * 86400;
63
- console.warn(
63
+ if (verbose) console.warn(
64
64
  `⚠️ No 'datasetStart'/'datasetEnd' set — dataset window anchored to today's date and will shift across runs. Pin both for full determinism.`
65
65
  );
66
66
  return { datasetStartUnix: fallbackStart, datasetEndUnix: todayStart, numDays: fallbackNumDays };
@@ -160,7 +160,7 @@ function stripKilledConfigKeys(config) {
160
160
  const found = KILLED_CONFIG_KEYS.filter(k => config[k] !== undefined && config[k] !== null);
161
161
  if (!found.length) return;
162
162
  for (const k of found) delete config[k];
163
- if (config.verbose !== false) {
163
+ if (config.verbose) {
164
164
  console.warn(
165
165
  `⚠️ dungeon-master 1.4 removed engine support for: ${found.join(', ')}. ` +
166
166
  `These config keys are silently ignored. Recreate via hooks (see lib/hook-patterns/* once Phase 4 lands).`
@@ -346,7 +346,7 @@ export function validateDungeonConfig(config) {
346
346
  // ── Resolve dataset window ──
347
347
  // Preferred path: explicit datasetStart + datasetEnd → pinned, deterministic window.
348
348
  // Fallback: numDays only → today_start - numDays back (sliding, warn-emitted).
349
- const windowResolution = resolveDatasetWindow(config.datasetStart, config.datasetEnd, config.numDays);
349
+ const windowResolution = resolveDatasetWindow(config.datasetStart, config.datasetEnd, config.numDays, verbose);
350
350
  const datasetStartUnix = windowResolution.datasetStartUnix;
351
351
  const datasetEndUnix = windowResolution.datasetEndUnix;
352
352
  numDays = windowResolution.numDays;
@@ -378,7 +378,7 @@ export function validateDungeonConfig(config) {
378
378
  // only avgEventsPerUserPerDay would never trigger auto-batch.
379
379
  if (numEvents >= 2_000_000 && config.batchSize === undefined) {
380
380
  batchSize = 1_000_000;
381
- console.warn(`⚠️ Auto-enabling batch mode: numEvents (${numEvents.toLocaleString()}) >= 2M. Using batchSize of ${batchSize.toLocaleString()}.`);
381
+ if (verbose) console.warn(`⚠️ Auto-enabling batch mode: numEvents (${numEvents.toLocaleString()}) >= 2M. Using batchSize of ${batchSize.toLocaleString()}.`);
382
382
  }
383
383
 
384
384
  // Resolve soup presets (intra-week / intra-day shape — must happen after numDays is computed)
@@ -422,7 +422,7 @@ export function validateDungeonConfig(config) {
422
422
  throw new Error('Hook string did not evaluate to a function');
423
423
  }
424
424
  } catch (error) {
425
- if (config.verbose !== false) {
425
+ if (verbose) {
426
426
  console.warn(`\u26a0\ufe0f Failed to convert hook string to function: ${error.message}`);
427
427
  console.warn('Using default pass-through hook');
428
428
  }
@@ -432,7 +432,7 @@ export function validateDungeonConfig(config) {
432
432
 
433
433
  // Ensure hook is a function
434
434
  if (typeof hook !== 'function') {
435
- if (config.verbose !== false) console.warn('\u26a0\ufe0f Hook is not a function, using default pass-through hook');
435
+ if (verbose) console.warn('\u26a0\ufe0f Hook is not a function, using default pass-through hook');
436
436
  hook = (record) => record;
437
437
  }
438
438
 
@@ -555,7 +555,7 @@ export function validateDungeonConfig(config) {
555
555
 
556
556
  // Warn if isAuthEvent is set but avgDevicePerUser=0 — pre-auth device_only
557
557
  // stamping degrades to user_id via the floor guard, defeating the identity model.
558
- if (avgDevicePerUser === 0 && validatedEvents.some(e => e.isAuthEvent)) {
558
+ if (verbose && avgDevicePerUser === 0 && validatedEvents.some(e => e.isAuthEvent)) {
559
559
  console.warn(
560
560
  `⚠️ isAuthEvent requires avgDevicePerUser >= 1 to produce pre-auth anonymous events. ` +
561
561
  `Set avgDevicePerUser or hasAnonIds: true.`
@@ -50,6 +50,7 @@ export async function createHookArray(arr = [], opts) {
50
50
  let writeDir;
51
51
  let isBatchMode = runtime.isBatchMode || false;
52
52
  let isWriting = false; // Prevent concurrent writes
53
+ const writtenFiles = [];
53
54
 
54
55
  // Determine write directory
55
56
  const dataFolder = path.resolve("./data");
@@ -175,6 +176,7 @@ export async function createHookArray(arr = [], opts) {
175
176
 
176
177
  // Write to disk/cloud - always blocking to prevent OOM
177
178
  const writeResult = await FILE_CONN(() => writeToDisk(dataToWrite, { writePath }));
179
+ writtenFiles.push(writePath);
178
180
  return writeResult;
179
181
  } finally {
180
182
  isWriting = false; // Release the lock
@@ -231,6 +233,7 @@ export async function createHookArray(arr = [], opts) {
231
233
  const dataToWrite = [...arr];
232
234
  arr.length = 0; // Clear array after copying data
233
235
  await FILE_CONN(() => writeToDisk(dataToWrite, { writePath }));
236
+ writtenFiles.push(writePath);
234
237
  // Data now lives on disk, not in arr — mirror what transformThenPush
235
238
  // does when crossing BATCH_SIZE so the Mixpanel sender knows to read
236
239
  // from disk instead of from the (now empty) in-memory array.
@@ -249,6 +252,7 @@ export async function createHookArray(arr = [], opts) {
249
252
  enrichedArray.flush = flush;
250
253
  enrichedArray.getWriteDir = getWriteDir;
251
254
  enrichedArray.getWritePath = getWritePath;
255
+ enrichedArray.getWrittenFiles = () => [...writtenFiles];
252
256
 
253
257
  // Add additional properties from rest
254
258
  for (const key in rest) {
@@ -384,7 +388,7 @@ export class StorageManager {
384
388
  const batchSize = config.batchSize || 1_000_000;
385
389
  const numEvents = config.numEvents || 0;
386
390
 
387
- if (batchSize < numEvents) {
391
+ if (batchSize < numEvents && config.verbose) {
388
392
  console.warn(
389
393
  `⚠️ writeToDisk is false but batchSize (${batchSize.toLocaleString()}) < numEvents (${numEvents.toLocaleString()}). ` +
390
394
  `Batch files will be written to disk temporarily to avoid OOM. ` +
@@ -6,7 +6,7 @@
6
6
  /** @typedef {import('../../types').Context} Context */
7
7
 
8
8
  import dayjs from "dayjs";
9
- import { comma, ls, rm } from "ak-tools";
9
+ import { comma, rm } from "ak-tools";
10
10
  import * as u from "../utils/utils.js";
11
11
  import mp from "mixpanel-import";
12
12
 
@@ -15,6 +15,8 @@ import mp from "mixpanel-import";
15
15
  * @param {Context} context - Context object containing config, storage, etc.
16
16
  * @returns {Promise<Object>} Import results for all data types
17
17
  */
18
+ export { collectWrittenFiles };
19
+
18
20
  export async function sendToMixpanel(context) {
19
21
  const { config, storage } = context;
20
22
  const {
@@ -70,11 +72,9 @@ export async function sendToMixpanel(context) {
70
72
  log(` Events`);
71
73
  let eventDataToImport = u.deepClone(eventData);
72
74
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && eventData && eventData.length === 0);
73
- if (shouldReadFromFiles && eventData?.getWriteDir) {
74
- const writeDir = eventData.getWriteDir();
75
- const files = await ls(writeDir);
76
- // @ts-ignore
77
- eventDataToImport = files.filter(f => f.includes('-EVENTS'));
75
+ if (shouldReadFromFiles && eventData?.getWrittenFiles) {
76
+ const files = eventData.getWrittenFiles();
77
+ if (files.length > 0) eventDataToImport = files;
78
78
  }
79
79
  const imported = await mp(creds, eventDataToImport, {
80
80
  recordType: "event",
@@ -89,11 +89,9 @@ export async function sendToMixpanel(context) {
89
89
  log(` User Profiles`);
90
90
  let userProfilesToImport = u.deepClone(userProfilesData);
91
91
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && userProfilesData && userProfilesData.length === 0);
92
- if (shouldReadFromFiles && userProfilesData?.getWriteDir) {
93
- const writeDir = userProfilesData.getWriteDir();
94
- const files = await ls(writeDir);
95
- // @ts-ignore
96
- userProfilesToImport = files.filter(f => f.includes('-USERS'));
92
+ if (shouldReadFromFiles && userProfilesData?.getWrittenFiles) {
93
+ const files = userProfilesData.getWrittenFiles();
94
+ if (files.length > 0) userProfilesToImport = files;
97
95
  }
98
96
  const imported = await mp(creds, userProfilesToImport, {
99
97
  recordType: "user",
@@ -108,11 +106,9 @@ export async function sendToMixpanel(context) {
108
106
  log(` Ad Spend`);
109
107
  let adSpendDataToImport = u.deepClone(adSpendData);
110
108
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && adSpendData && adSpendData.length === 0);
111
- if (shouldReadFromFiles && adSpendData?.getWriteDir) {
112
- const writeDir = adSpendData.getWriteDir();
113
- const files = await ls(writeDir);
114
- // @ts-ignore
115
- adSpendDataToImport = files.filter(f => f.includes('-ADSPEND'));
109
+ if (shouldReadFromFiles && adSpendData?.getWrittenFiles) {
110
+ const files = adSpendData.getWrittenFiles();
111
+ if (files.length > 0) adSpendDataToImport = files;
116
112
  }
117
113
  const imported = await mp(creds, adSpendDataToImport, {
118
114
  recordType: "event",
@@ -130,11 +126,9 @@ export async function sendToMixpanel(context) {
130
126
  log(` Group Profiles (${groupKey})`);
131
127
  let groupProfilesToImport = u.deepClone(groupEntity);
132
128
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEntity.length === 0);
133
- if (shouldReadFromFiles && groupEntity?.getWriteDir) {
134
- const writeDir = groupEntity.getWriteDir();
135
- const files = await ls(writeDir);
136
- // @ts-ignore
137
- groupProfilesToImport = files.filter(f => f.includes(`-${groupKey}-GROUPS`));
129
+ if (shouldReadFromFiles && groupEntity?.getWrittenFiles) {
130
+ const files = groupEntity.getWrittenFiles();
131
+ if (files.length > 0) groupProfilesToImport = files;
138
132
  }
139
133
  const imported = await mp({ token, groupKey }, groupProfilesToImport, {
140
134
  recordType: "group",
@@ -151,11 +145,9 @@ export async function sendToMixpanel(context) {
151
145
  log(` Group Events`);
152
146
  let groupEventDataToImport = u.deepClone(groupEventData);
153
147
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEventData.length === 0);
154
- if (shouldReadFromFiles && groupEventData?.getWriteDir) {
155
- const writeDir = groupEventData.getWriteDir();
156
- const files = await ls(writeDir);
157
- // @ts-ignore
158
- groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS'));
148
+ if (shouldReadFromFiles && groupEventData?.getWrittenFiles) {
149
+ const files = groupEventData.getWrittenFiles();
150
+ if (files.length > 0) groupEventDataToImport = files;
159
151
  }
160
152
  const imported = await mp(creds, groupEventDataToImport, {
161
153
  recordType: "event",
@@ -174,11 +166,9 @@ export async function sendToMixpanel(context) {
174
166
  log(` SCD: ${scdKey}`);
175
167
  let scdDataToImport = u.deepClone(scdEntity);
176
168
  const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && scdEntity && scdEntity.length === 0);
177
- if (shouldReadFromFiles && scdEntity?.getWriteDir) {
178
- const writeDir = scdEntity.getWriteDir();
179
- const files = await ls(writeDir);
180
- // @ts-ignore
181
- scdDataToImport = files.filter(f => f.includes(`-${scdKey}-SCD`))?.pop();
169
+ if (shouldReadFromFiles && scdEntity?.getWrittenFiles) {
170
+ const files = scdEntity.getWrittenFiles();
171
+ if (files.length > 0) scdDataToImport = files;
182
172
  }
183
173
 
184
174
  /** @type {"string" | "number" | "boolean"} */
@@ -231,27 +221,11 @@ export async function sendToMixpanel(context) {
231
221
 
232
222
  log(`${'─'.repeat(50)}\n`);
233
223
 
234
- // Clean up batch files if needed
224
+ // Clean up batch files if needed (writeToDisk=false but batch mode wrote temp files)
235
225
  if (!writeToDisk && isBATCH_MODE) {
236
- const writeDir = eventData?.getWriteDir?.() || userProfilesData?.getWriteDir?.();
237
- if (writeDir) {
238
- const configName = context.config.name;
239
- const listDir = await ls(writeDir);
240
- // @ts-ignore
241
- const files = listDir.filter(f => {
242
- if (configName && !f.includes(configName)) return false;
243
- return f.includes('-EVENTS') ||
244
- f.includes('-USERS') ||
245
- f.includes('-ADSPEND') ||
246
- f.includes('-GROUPS') ||
247
- f.includes('-GROUP-EVENTS') ||
248
- f.includes('-SCD') ||
249
- f.includes('-MIRROR') ||
250
- f.includes('-LOOKUP');
251
- });
252
- for (const file of files) {
253
- await rm(file);
254
- }
226
+ const allFiles = collectWrittenFiles(storage);
227
+ for (const file of allFiles) {
228
+ await rm(file);
255
229
  }
256
230
  }
257
231
 
@@ -324,4 +298,25 @@ function logProblems(problems) {
324
298
  }
325
299
  }
326
300
  log('');
301
+ }
302
+
303
+ /**
304
+ * Collect all written file paths from every storage container.
305
+ * @param {import('../../types').Storage} storage
306
+ * @returns {string[]}
307
+ */
308
+ function collectWrittenFiles(storage) {
309
+ const files = [];
310
+ for (const container of [storage.eventData, storage.userProfilesData, storage.adSpendData,
311
+ storage.mirrorEventData, storage.groupEventData]) {
312
+ if (container?.getWrittenFiles) files.push(...container.getWrittenFiles());
313
+ }
314
+ for (const arr of [storage.groupProfilesData, storage.scdTableData, storage.lookupTableData]) {
315
+ if (Array.isArray(arr)) {
316
+ for (const c of arr) {
317
+ if (c?.getWrittenFiles) files.push(...c.getWrittenFiles());
318
+ }
319
+ }
320
+ }
321
+ return files;
327
322
  }
@@ -31,7 +31,7 @@ export const MACRO_PRESETS = {
31
31
  */
32
32
  flat: {
33
33
  bornRecentBias: 0,
34
- percentUsersBornInDataset: 15,
34
+ percentUsersBornInDataset: 50,
35
35
  preExistingSpread: 'uniform',
36
36
  },
37
37
 
@@ -41,7 +41,7 @@ export const MACRO_PRESETS = {
41
41
  */
42
42
  steady: {
43
43
  bornRecentBias: 0.1,
44
- percentUsersBornInDataset: 10,
44
+ percentUsersBornInDataset: 35,
45
45
  preExistingSpread: 'uniform',
46
46
  },
47
47
 
@@ -51,7 +51,7 @@ export const MACRO_PRESETS = {
51
51
  */
52
52
  growth: {
53
53
  bornRecentBias: 0.3,
54
- percentUsersBornInDataset: 25,
54
+ percentUsersBornInDataset: 60,
55
55
  preExistingSpread: 'pinned',
56
56
  },
57
57
 
@@ -61,7 +61,7 @@ export const MACRO_PRESETS = {
61
61
  */
62
62
  viral: {
63
63
  bornRecentBias: 0.6,
64
- percentUsersBornInDataset: 50,
64
+ percentUsersBornInDataset: 95,
65
65
  preExistingSpread: 'pinned',
66
66
  },
67
67
 
@@ -71,7 +71,7 @@ export const MACRO_PRESETS = {
71
71
  */
72
72
  decline: {
73
73
  bornRecentBias: -0.3,
74
- percentUsersBornInDataset: 5,
74
+ percentUsersBornInDataset: 25,
75
75
  preExistingSpread: 'uniform',
76
76
  },
77
77
  };
@@ -1597,6 +1597,15 @@ function assignSessionIds(events, timeoutMinutes = 30) {
1597
1597
  return events;
1598
1598
  }
1599
1599
 
1600
+ async function deleteFile(filePath) {
1601
+ if (filePath.startsWith('gs://')) {
1602
+ const { bucket, file } = parseGCSUri(filePath);
1603
+ await storage.bucket(bucket).file(file).delete({ ignoreNotFound: true });
1604
+ } else {
1605
+ await fs.promises.unlink(filePath).catch(() => {});
1606
+ }
1607
+ }
1608
+
1600
1609
  export {
1601
1610
  pick,
1602
1611
  date,
@@ -1651,4 +1660,5 @@ export {
1651
1660
  assignSessionIds,
1652
1661
  bunchIntoSessions,
1653
1662
  setDatasetNow,
1663
+ deleteFile,
1654
1664
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ak--47/dungeon-master",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "generate fancy datasets",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/types.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface Dungeon {
19
19
  // ── Core Parameters ──
20
20
  /** Optional dungeon version. Not used by the engine — serves as metadata for tracking revisions when configs are saved/shared. */
21
21
  version?: string | number;
22
+ /** Optional app or dataset name. Not used by the engine — available for logging and metadata when set. */
23
+ appName?: string;
22
24
  /** Mixpanel project token. If provided, data will be imported to Mixpanel after generation. */
23
25
  token?: string;
24
26
  /** RNG seed for reproducible output. Same seed + concurrency=1 = identical data. */
@@ -109,8 +111,10 @@ export interface Dungeon {
109
111
  hasDesktopDevices?: boolean;
110
112
  /** If true, events include browser properties. */
111
113
  hasBrowser?: boolean;
112
- /** If true (default), writes output files to ./data/. Can also be a directory path string. */
114
+ /** If true (default), writes output files to ./data/. Can also be a directory path string or gs:// URI. */
113
115
  writeToDisk?: boolean | string;
116
+ /** If true, deletes all written files (local and GCS) at end of run regardless of import success/failure. Default: false. */
117
+ cleanup?: boolean;
114
118
  /** If true, gzip-compresses output files. */
115
119
  gzip?: boolean;
116
120
  /** If true, prints progress to stdout during generation. */
@@ -220,7 +224,7 @@ export interface Dungeon {
220
224
  // ── Distribution Controls ──
221
225
  // These three knobs are normally set by the `macro` preset (default "flat").
222
226
  // Setting them on the dungeon config directly overrides the preset's value.
223
- /** Percentage of users whose account creation falls within the dataset window (vs. pre-existing). Default (from macro: "flat"): 15 */
227
+ /** Percentage of users whose account creation falls within the dataset window (vs. pre-existing). Default (from macro: "flat"): 50 */
224
228
  percentUsersBornInDataset?: number;
225
229
  /** Bias for birth dates of users born in dataset. -1..1; negative = early skew, positive = recent skew, 0 = uniform. Default (from macro: "flat"): 0 */
226
230
  bornRecentBias?: number;
@@ -541,6 +545,8 @@ export interface HookedArray<T> extends Array<T> {
541
545
  getWriteDir: () => string;
542
546
  /** Absolute path (with extension) of the next batch file. */
543
547
  getWritePath: () => string;
548
+ /** Returns all file paths written by this container during the current run. */
549
+ getWrittenFiles: () => string[];
544
550
  /** SCD prop name this array carries (only set on SCD HookedArrays). */
545
551
  scdKey?: string;
546
552
  /** Entity type for SCDs ("user" or a group key). */