@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.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- 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 =
|
|
4
|
-
const num_users =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
if (type === "funnel-
|
|
646
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
141
|
-
|
|
142
|
-
const
|
|
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
|
-
//
|
|
145
|
-
|
|
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
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
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) {
|