@ak--47/dungeon-master 1.3.0 → 1.4.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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-travel";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
- const NOW = dayjs();
20
- const DATASET_START = NOW.subtract(num_days, "days");
21
-
22
19
  /** @typedef {import("../../types").Dungeon} Config */
23
20
 
24
21
  const hotelIds = v.range(1, 200).map(() => `HTL_${v.uid(6)}`);
@@ -32,7 +29,7 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
32
29
  * StayQuest — a hotel booking platform for business and leisure travelers.
33
30
  * Users search destinations, compare hotels, book rooms, and leave reviews.
34
31
  *
35
- * - 5,000 users over 100 days, ~600K events
32
+ * - 5,000 users over 120 days, ~600K events
36
33
  * - Segments: business travelers (weekday), leisure families, luxury, budget
37
34
  * - Core loop: search → view hotel → compare → book → stay → review
38
35
  * - Revenue: commission per booking + premium loyalty membership
@@ -48,7 +45,16 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
48
45
 
49
46
  /**
50
47
  * ═══════════════════════════════════════════════════════════════
51
- * ANALYTICS HOOKS (8 hooks)
48
+ * ANALYTICS HOOKS (10 hooks)
49
+ *
50
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
51
+ * raw-prop breakdowns (booking_window, day, segment) or behavioral cohorts.
52
+ *
53
+ * Adds 9. BOOKING TIME-TO-CONVERT (Business 1.35x faster, Budget 1.25x slower)
54
+ * [funnel-post: visible only in Mixpanel funnel median TTC; cross-event
55
+ * MIN→MIN SQL queries do NOT show this]
56
+ * and 10. HOTEL-VIEWED MAGIC NUMBER (sweet 5-10 → +30% nightly_rate;
57
+ * over 11+ → drop 35% of bookings).
52
58
  * ═══════════════════════════════════════════════════════════════
53
59
  *
54
60
  * ───────────────────────────────────────────────────────────────
@@ -204,12 +210,16 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
204
210
 
205
211
  /** @type {Config} */
206
212
  const config = {
213
+ version: 2,
207
214
  token,
208
215
  seed: SEED,
209
- numDays: num_days,
216
+ datasetStart: "2026-01-01T00:00:00Z",
217
+ datasetEnd: "2026-05-01T23:59:59Z",
218
+ // numDays: num_days,
210
219
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
211
220
  numUsers: num_users,
212
- hasAnonIds: false,
221
+ hasAnonIds: true,
222
+ avgDevicePerUser: 2,
213
223
  hasSessionIds: true,
214
224
  format: "json",
215
225
  gzip: true,
@@ -242,6 +252,7 @@ const config = {
242
252
  event: "account created",
243
253
  weight: 1,
244
254
  isFirstEvent: true,
255
+ isAuthEvent: true,
245
256
  properties: {
246
257
  signup_source: ["organic", "google", "instagram", "tripadvisor", "referral", "email_campaign"],
247
258
  },
@@ -639,11 +650,25 @@ const config = {
639
650
  }
640
651
  }
641
652
 
642
- // ── HOOK 8: REPEAT DESTINATION CLUSTERING (funnel-pre)
643
- // conversionRate modifications moved to everything hook (event filtering)
644
- // to avoid dilution by organic events
645
- if (type === "funnel-pre") {
646
- // prop-setting only; no conversionRate changes
653
+ // HOOK 9 (T2C): BOOKING TIME-TO-CONVERT (funnel-post)
654
+ // Business travelers complete the Search-to-Book funnel 1.35x faster
655
+ // (factor 0.74); leisure_family/budget customers 1.25x slower (1.25).
656
+ if (type === "funnel-post") {
657
+ const segment = meta?.profile?.customer_segment;
658
+ if (Array.isArray(record) && record.length > 1) {
659
+ const factor = (
660
+ segment === "business_traveler" ? 0.74 :
661
+ segment === "budget_hunter" || segment === "leisure_family" ? 1.25 :
662
+ 1.0
663
+ );
664
+ if (factor !== 1.0) {
665
+ for (let i = 1; i < record.length; i++) {
666
+ const prev = dayjs(record[i - 1].time);
667
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
668
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
669
+ }
670
+ }
671
+ }
647
672
  }
648
673
 
649
674
  // ── HOOK 1: WEEKEND LEISURE SURGE ────────────
@@ -686,10 +711,11 @@ const config = {
686
711
  });
687
712
 
688
713
  // ── HOOK 2: ADVANCE BOOKING DISCOUNT ─────
714
+ const datasetEndForBooking = dayjs.unix(meta.datasetEnd);
689
715
  events.forEach(e => {
690
716
  if (e.event === "booking completed") {
691
717
  const eventTime = dayjs(e.time);
692
- const daysUntilEnd = NOW.diff(eventTime, "days");
718
+ const daysUntilEnd = datasetEndForBooking.diff(eventTime, "days");
693
719
  if (daysUntilEnd > 21) {
694
720
  e.booking_window = "advance";
695
721
  e.nightly_rate = Math.floor((e.nightly_rate || 150) * 0.8);
@@ -705,12 +731,11 @@ const config = {
705
731
  // business_traveler or luxury_seeker to simulate their lower funnel
706
732
  // conversion (was conversionRate * 1.3 / 1.15 in funnel-pre)
707
733
  const segment = profile && profile.customer_segment;
708
- if (segment !== "business_traveler" && segment !== "luxury_seeker") {
734
+ if (segment !== "business_traveler" && segment !== "luxury_seeker"
735
+ && chance.bool({ likelihood: 25 })) {
709
736
  for (let i = events.length - 1; i >= 0; i--) {
710
737
  if (events[i].event === "booking completed") {
711
- if (chance.bool({ likelihood: 25 })) {
712
- events.splice(i, 1);
713
- }
738
+ events.splice(i, 1);
714
739
  }
715
740
  }
716
741
  }
@@ -776,7 +801,8 @@ const config = {
776
801
  }
777
802
  }
778
803
 
779
- // ── HOOK 6: REVIEW QUALITY BY STAY RATING ────────
804
+ // HOOK 6: REVIEW QUALITY BY STAY RATING — high avg ratings get
805
+ // review_length 1.5x; low avg ratings get 0.5x. Mutates raw prop.
780
806
  let totalRating = 0;
781
807
  let ratingCount = 0;
782
808
  events.forEach(e => {
@@ -796,6 +822,26 @@ const config = {
796
822
  }
797
823
  });
798
824
 
825
+ // HOOK 10: HOTEL-VIEWED MAGIC NUMBER (in-funnel, no flags)
826
+ // Sweet 5-10 hotel-viewed events → +30% on booking nightly_rate
827
+ // (decisive comparison shoppers book higher-tier rooms).
828
+ // Over 11+ → drop 35% of booking-completed events (analysis
829
+ // paralysis blocks conversion).
830
+ const hotelViews = events.filter(e => e.event === "hotel viewed").length;
831
+ if (hotelViews >= 5 && hotelViews <= 10) {
832
+ events.forEach(e => {
833
+ if (e.event === "booking completed" && typeof e.nightly_rate === "number") {
834
+ e.nightly_rate = Math.round(e.nightly_rate * 1.3);
835
+ }
836
+ });
837
+ } else if (hotelViews >= 11) {
838
+ for (let i = events.length - 1; i >= 0; i--) {
839
+ if (events[i].event === "booking completed" && chance.bool({ likelihood: 35 })) {
840
+ events.splice(i, 1);
841
+ }
842
+ }
843
+ }
844
+
799
845
  return record;
800
846
  }
801
847
 
package/index.js CHANGED
@@ -25,7 +25,7 @@ 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 } from './lib/utils/utils.js';
28
+ import { initChance, setDatasetNow } from './lib/utils/utils.js';
29
29
 
30
30
  // External dependencies
31
31
  import dayjs from "dayjs";
@@ -33,12 +33,8 @@ import utc from "dayjs/plugin/utc.js";
33
33
  import { timer } from 'ak-tools';
34
34
  import { dataLogger as logger } from './lib/utils/logger.js';
35
35
 
36
- // Initialize dayjs and time constants
36
+ // Initialize dayjs (window anchors are now resolved per-config in config-validator)
37
37
  dayjs.extend(utc);
38
- const FIXED_NOW = dayjs('2024-02-02').unix();
39
- global.FIXED_NOW = FIXED_NOW;
40
- let FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(90, 'd').unix();
41
- global.FIXED_BEGIN = FIXED_BEGIN;
42
38
 
43
39
 
44
40
  /**
@@ -134,18 +130,21 @@ async function runDungeon(config) {
134
130
  initChance(config.seed);
135
131
  }
136
132
 
137
- // Step 1: Validate and enrich configuration
133
+ // Step 1: Validate and enrich configuration (resolves dataset window)
138
134
  validatedConfig = validateDungeonConfig(config);
139
135
 
140
- // Compute FIXED_BEGIN from validated numDays
141
- const configNumDays = validatedConfig.numDays || 30;
142
- const fixedBegin = dayjs.unix(FIXED_NOW).subtract(configNumDays, 'd').unix();
136
+ // validateDungeonConfig always resolves these to unix seconds, but the
137
+ // public Dungeon type accepts string | number on input. Narrow here.
138
+ const fixedNow = /** @type {number} */ (validatedConfig.datasetEnd);
139
+ const fixedBegin = /** @type {number} */ (validatedConfig.datasetStart);
143
140
 
144
- // Keep globals for backwards compatibility with tests/dungeons that read them
145
- global.FIXED_BEGIN = fixedBegin;
141
+ // Anchor the wall-clock-free reference used by date()/day() helpers in
142
+ // dungeon configs. Without this, those factories produce values relative
143
+ // to process-start, which leaks wall-clock time into the output.
144
+ setDatasetNow(fixedNow);
146
145
 
147
146
  // Step 2: Create context with validated config (pass time constants explicitly)
148
- const context = createContext(validatedConfig, null, { fixedNow: FIXED_NOW, fixedBegin });
147
+ const context = createContext(validatedConfig, null, { fixedNow, fixedBegin });
149
148
 
150
149
  // Step 3: Initialize storage containers
151
150
  const storageManager = new StorageManager(context);
@@ -237,11 +236,12 @@ async function generateAdSpendData(context) {
237
236
  const { config, storage } = context;
238
237
  const { numDays } = config;
239
238
 
240
- const timeShift = context.TIME_SHIFT_SECONDS;
241
239
  for (let day = 0; day < numDays; day++) {
242
- const fixedDay = dayjs.unix(context.FIXED_BEGIN).add(day, 'day').unix();
243
- const shiftedDay = Math.min(fixedDay + timeShift, context.MAX_TIME);
244
- const targetDay = dayjs.unix(shiftedDay).toISOString();
240
+ const dayUnix = Math.min(
241
+ dayjs.unix(context.FIXED_BEGIN).add(day, 'day').unix(),
242
+ context.FIXED_NOW
243
+ );
244
+ const targetDay = dayjs.unix(dayUnix).toISOString();
245
245
  const adSpendEvents = await makeAdSpend(context, targetDay);
246
246
 
247
247
  if (adSpendEvents.length > 0) {