@ak--47/dungeon-master 1.3.0 → 1.3.1
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/dungeons/technical/array-of-object-lookup.js +0 -2
- 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 +192 -133
- package/dungeons/vertical/community.js +64 -34
- package/dungeons/vertical/crypto.js +333 -224
- package/dungeons/vertical/dating.js +228 -282
- package/dungeons/vertical/devtools.js +164 -62
- package/dungeons/vertical/ecommerce.js +240 -91
- package/dungeons/vertical/education.js +328 -310
- package/dungeons/vertical/fintech.js +437 -311
- package/dungeons/vertical/fitness.js +141 -58
- package/dungeons/vertical/food-delivery.js +298 -318
- package/dungeons/vertical/gaming.js +378 -231
- package/dungeons/vertical/healthcare.js +140 -60
- package/dungeons/vertical/insurance-application.js +168 -73
- package/dungeons/vertical/logistics.js +95 -6
- package/dungeons/vertical/marketplace.js +137 -51
- package/dungeons/vertical/media.js +241 -386
- package/dungeons/vertical/real-estate.js +322 -315
- package/dungeons/vertical/sass.js +253 -263
- package/dungeons/vertical/social.js +262 -206
- package/dungeons/vertical/travel.js +59 -16
- package/index.js +17 -17
- package/lib/core/config-validator.js +93 -21
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- 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/orchestrators/user-loop.js +47 -47
- package/lib/utils/utils.js +71 -39
- package/package.json +1 -1
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +38 -15
- 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
|
@@ -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)}`);
|
|
@@ -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
|
* ───────────────────────────────────────────────────────────────
|
|
@@ -206,7 +212,9 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
|
|
|
206
212
|
const config = {
|
|
207
213
|
token,
|
|
208
214
|
seed: SEED,
|
|
209
|
-
|
|
215
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
216
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
217
|
+
// numDays: num_days,
|
|
210
218
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
211
219
|
numUsers: num_users,
|
|
212
220
|
hasAnonIds: false,
|
|
@@ -639,11 +647,25 @@ const config = {
|
|
|
639
647
|
}
|
|
640
648
|
}
|
|
641
649
|
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
if (type === "funnel-
|
|
646
|
-
|
|
650
|
+
// HOOK 9 (T2C): BOOKING TIME-TO-CONVERT (funnel-post)
|
|
651
|
+
// Business travelers complete the Search-to-Book funnel 1.35x faster
|
|
652
|
+
// (factor 0.74); leisure_family/budget customers 1.25x slower (1.25).
|
|
653
|
+
if (type === "funnel-post") {
|
|
654
|
+
const segment = meta?.profile?.customer_segment;
|
|
655
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
656
|
+
const factor = (
|
|
657
|
+
segment === "business_traveler" ? 0.74 :
|
|
658
|
+
segment === "budget_hunter" || segment === "leisure_family" ? 1.25 :
|
|
659
|
+
1.0
|
|
660
|
+
);
|
|
661
|
+
if (factor !== 1.0) {
|
|
662
|
+
for (let i = 1; i < record.length; i++) {
|
|
663
|
+
const prev = dayjs(record[i - 1].time);
|
|
664
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
665
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
647
669
|
}
|
|
648
670
|
|
|
649
671
|
// ── HOOK 1: WEEKEND LEISURE SURGE ────────────
|
|
@@ -686,10 +708,11 @@ const config = {
|
|
|
686
708
|
});
|
|
687
709
|
|
|
688
710
|
// ── HOOK 2: ADVANCE BOOKING DISCOUNT ─────
|
|
711
|
+
const datasetEndForBooking = dayjs.unix(meta.datasetEnd);
|
|
689
712
|
events.forEach(e => {
|
|
690
713
|
if (e.event === "booking completed") {
|
|
691
714
|
const eventTime = dayjs(e.time);
|
|
692
|
-
const daysUntilEnd =
|
|
715
|
+
const daysUntilEnd = datasetEndForBooking.diff(eventTime, "days");
|
|
693
716
|
if (daysUntilEnd > 21) {
|
|
694
717
|
e.booking_window = "advance";
|
|
695
718
|
e.nightly_rate = Math.floor((e.nightly_rate || 150) * 0.8);
|
|
@@ -705,12 +728,11 @@ const config = {
|
|
|
705
728
|
// business_traveler or luxury_seeker to simulate their lower funnel
|
|
706
729
|
// conversion (was conversionRate * 1.3 / 1.15 in funnel-pre)
|
|
707
730
|
const segment = profile && profile.customer_segment;
|
|
708
|
-
if (segment !== "business_traveler" && segment !== "luxury_seeker"
|
|
731
|
+
if (segment !== "business_traveler" && segment !== "luxury_seeker"
|
|
732
|
+
&& chance.bool({ likelihood: 25 })) {
|
|
709
733
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
710
734
|
if (events[i].event === "booking completed") {
|
|
711
|
-
|
|
712
|
-
events.splice(i, 1);
|
|
713
|
-
}
|
|
735
|
+
events.splice(i, 1);
|
|
714
736
|
}
|
|
715
737
|
}
|
|
716
738
|
}
|
|
@@ -776,7 +798,8 @@ const config = {
|
|
|
776
798
|
}
|
|
777
799
|
}
|
|
778
800
|
|
|
779
|
-
//
|
|
801
|
+
// HOOK 6: REVIEW QUALITY BY STAY RATING — high avg ratings get
|
|
802
|
+
// review_length 1.5x; low avg ratings get 0.5x. Mutates raw prop.
|
|
780
803
|
let totalRating = 0;
|
|
781
804
|
let ratingCount = 0;
|
|
782
805
|
events.forEach(e => {
|
|
@@ -796,6 +819,26 @@ const config = {
|
|
|
796
819
|
}
|
|
797
820
|
});
|
|
798
821
|
|
|
822
|
+
// HOOK 10: HOTEL-VIEWED MAGIC NUMBER (in-funnel, no flags)
|
|
823
|
+
// Sweet 5-10 hotel-viewed events → +30% on booking nightly_rate
|
|
824
|
+
// (decisive comparison shoppers book higher-tier rooms).
|
|
825
|
+
// Over 11+ → drop 35% of booking-completed events (analysis
|
|
826
|
+
// paralysis blocks conversion).
|
|
827
|
+
const hotelViews = events.filter(e => e.event === "hotel viewed").length;
|
|
828
|
+
if (hotelViews >= 5 && hotelViews <= 10) {
|
|
829
|
+
events.forEach(e => {
|
|
830
|
+
if (e.event === "booking completed" && typeof e.nightly_rate === "number") {
|
|
831
|
+
e.nightly_rate = Math.round(e.nightly_rate * 1.3);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
} else if (hotelViews >= 11) {
|
|
835
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
836
|
+
if (events[i].event === "booking completed" && chance.bool({ likelihood: 35 })) {
|
|
837
|
+
events.splice(i, 1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
799
842
|
return record;
|
|
800
843
|
}
|
|
801
844
|
|
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) {
|
|
@@ -14,8 +14,80 @@ import * as u from "../utils/utils.js";
|
|
|
14
14
|
import { resolveSoup } from "../templates/soup-presets.js";
|
|
15
15
|
import { resolveMacro } from "../templates/macro-presets.js";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Resolve dataset window from config. Returns { datasetStartUnix, datasetEndUnix, numDays }.
|
|
19
|
+
* Three modes:
|
|
20
|
+
* 1. Both `datasetStart` AND `datasetEnd` provided → pin window. Recompute numDays from the
|
|
21
|
+
* window (ignore any user-supplied numDays — warn on conflict). Bit-exact deterministic.
|
|
22
|
+
* 2. Neither provided → fall back to (today_start - numDays, today_start). Deterministic
|
|
23
|
+
* within a single calendar day; slides across days. Warn once.
|
|
24
|
+
* 3. Exactly one provided → throw.
|
|
25
|
+
*
|
|
26
|
+
* Accepts ISO strings, unix seconds, or anything dayjs() can parse.
|
|
27
|
+
*
|
|
28
|
+
* @param {*} datasetStart
|
|
29
|
+
* @param {*} datasetEnd
|
|
30
|
+
* @param {number} [userNumDays]
|
|
31
|
+
* @returns {{ datasetStartUnix: number, datasetEndUnix: number, numDays: number }}
|
|
32
|
+
*/
|
|
33
|
+
function resolveDatasetWindow(datasetStart, datasetEnd, userNumDays) {
|
|
34
|
+
const hasStart = datasetStart !== undefined && datasetStart !== null;
|
|
35
|
+
const hasEnd = datasetEnd !== undefined && datasetEnd !== null;
|
|
36
|
+
|
|
37
|
+
if (hasStart !== hasEnd) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`datasetStart and datasetEnd must be specified together (got datasetStart=${datasetStart}, datasetEnd=${datasetEnd}). ` +
|
|
40
|
+
`Provide both to pin the window, or neither to fall back to numDays.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (hasStart && hasEnd) {
|
|
45
|
+
const startUnix = parseToUnix(datasetStart, 'datasetStart');
|
|
46
|
+
const endUnix = parseToUnix(datasetEnd, 'datasetEnd');
|
|
47
|
+
if (endUnix <= startUnix) {
|
|
48
|
+
throw new Error(`datasetEnd (${datasetEnd}) must be after datasetStart (${datasetStart}).`);
|
|
49
|
+
}
|
|
50
|
+
const derivedNumDays = Math.max(1, Math.round((endUnix - startUnix) / 86400));
|
|
51
|
+
if (userNumDays !== undefined && userNumDays !== null && userNumDays !== derivedNumDays) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`⚠️ datasetStart/datasetEnd take precedence; user-supplied numDays=${userNumDays} ignored, derived numDays=${derivedNumDays}.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { datasetStartUnix: startUnix, datasetEndUnix: endUnix, numDays: derivedNumDays };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback: anchor to today's start-of-day, walk back numDays
|
|
60
|
+
const fallbackNumDays = (typeof userNumDays === 'number' && userNumDays > 0) ? userNumDays : 30;
|
|
61
|
+
const todayStart = dayjs().startOf('day').unix();
|
|
62
|
+
const fallbackStart = todayStart - fallbackNumDays * 86400;
|
|
63
|
+
console.warn(
|
|
64
|
+
`⚠️ No 'datasetStart'/'datasetEnd' set — dataset window anchored to today's date and will shift across runs. Pin both for full determinism.`
|
|
65
|
+
);
|
|
66
|
+
return { datasetStartUnix: fallbackStart, datasetEndUnix: todayStart, numDays: fallbackNumDays };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse a value (ISO string, unix seconds, dayjs-parseable) into unix seconds.
|
|
71
|
+
* Throws if the value can't be parsed into a valid date.
|
|
72
|
+
* @param {*} value
|
|
73
|
+
* @param {string} fieldName
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
function parseToUnix(value, fieldName) {
|
|
77
|
+
// Treat numbers as unix seconds (or unix milliseconds if too large)
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
80
|
+
throw new Error(`${fieldName} must be a positive finite number (got ${value}).`);
|
|
81
|
+
}
|
|
82
|
+
// Heuristic: > 10^12 means milliseconds, otherwise seconds
|
|
83
|
+
return value > 1e12 ? Math.floor(value / 1000) : Math.floor(value);
|
|
84
|
+
}
|
|
85
|
+
const parsed = dayjs(value);
|
|
86
|
+
if (!parsed.isValid()) {
|
|
87
|
+
throw new Error(`${fieldName} could not be parsed as a date (got ${JSON.stringify(value)}).`);
|
|
88
|
+
}
|
|
89
|
+
return parsed.unix();
|
|
90
|
+
}
|
|
19
91
|
|
|
20
92
|
/**
|
|
21
93
|
* Infers funnels from the provided events
|
|
@@ -168,18 +240,18 @@ export function validateDungeonConfig(config) {
|
|
|
168
240
|
throw new Error(`numDays must be a positive number (got ${config.numDays})`);
|
|
169
241
|
}
|
|
170
242
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
// ── Resolve dataset window ──
|
|
244
|
+
// Preferred path: explicit datasetStart + datasetEnd → pinned, deterministic window.
|
|
245
|
+
// Fallback: numDays only → today_start - numDays back (sliding, warn-emitted).
|
|
246
|
+
const windowResolution = resolveDatasetWindow(config.datasetStart, config.datasetEnd, config.numDays);
|
|
247
|
+
const datasetStartUnix = windowResolution.datasetStartUnix;
|
|
248
|
+
const datasetEndUnix = windowResolution.datasetEndUnix;
|
|
249
|
+
numDays = windowResolution.numDays;
|
|
178
250
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
251
|
+
// Mirror window into legacy epoch* surface (still consumed by inferFunnels callers
|
|
252
|
+
// and any external code that read these from validated config).
|
|
253
|
+
epochStart = datasetStartUnix;
|
|
254
|
+
epochEnd = datasetEndUnix;
|
|
183
255
|
|
|
184
256
|
// Resolve event-rate primitive: avgEventsPerUserPerDay is the canonical knob.
|
|
185
257
|
// numEvents is supported as a fallback (legacy + total-volume target). Whichever
|
|
@@ -349,7 +421,7 @@ export function validateDungeonConfig(config) {
|
|
|
349
421
|
// Feature 2: World Events
|
|
350
422
|
let worldEvents = config.worldEvents || null;
|
|
351
423
|
if (worldEvents) {
|
|
352
|
-
worldEvents = resolveWorldEvents(worldEvents,
|
|
424
|
+
worldEvents = resolveWorldEvents(worldEvents, datasetStartUnix);
|
|
353
425
|
}
|
|
354
426
|
|
|
355
427
|
// Feature 3: Engagement Decay
|
|
@@ -391,7 +463,7 @@ export function validateDungeonConfig(config) {
|
|
|
391
463
|
// Feature 9: Anomalies
|
|
392
464
|
let anomalies = config.anomalies || null;
|
|
393
465
|
if (anomalies) {
|
|
394
|
-
anomalies = resolveAnomalies(anomalies,
|
|
466
|
+
anomalies = resolveAnomalies(anomalies, datasetStartUnix);
|
|
395
467
|
}
|
|
396
468
|
|
|
397
469
|
// Build final config object
|
|
@@ -407,6 +479,8 @@ export function validateDungeonConfig(config) {
|
|
|
407
479
|
avgEventsPerUserPerDay,
|
|
408
480
|
epochStart,
|
|
409
481
|
epochEnd,
|
|
482
|
+
datasetStart: datasetStartUnix,
|
|
483
|
+
datasetEnd: datasetEndUnix,
|
|
410
484
|
events: validatedEvents,
|
|
411
485
|
superProps,
|
|
412
486
|
userProps,
|
|
@@ -551,12 +625,11 @@ function validatePersonas(personas) {
|
|
|
551
625
|
/**
|
|
552
626
|
* Resolves world events to absolute timestamps
|
|
553
627
|
* @param {import('../../types').WorldEvent[]} worldEvents
|
|
554
|
-
* @param {number}
|
|
628
|
+
* @param {number} beginUnix - Dataset start (unix seconds)
|
|
555
629
|
* @returns {import('../../types').ResolvedWorldEvent[]}
|
|
556
630
|
*/
|
|
557
|
-
function resolveWorldEvents(worldEvents,
|
|
631
|
+
function resolveWorldEvents(worldEvents, beginUnix) {
|
|
558
632
|
if (!Array.isArray(worldEvents) || worldEvents.length === 0) return null;
|
|
559
|
-
const beginUnix = dayjs.unix(FIXED_NOW).subtract(numDays, 'day').unix();
|
|
560
633
|
|
|
561
634
|
return worldEvents.map(we => {
|
|
562
635
|
const startUnix = beginUnix + (we.startDay * 86400);
|
|
@@ -720,12 +793,11 @@ function resolveFeatures(features, numDays) {
|
|
|
720
793
|
/**
|
|
721
794
|
* Resolves anomaly configs with absolute timestamps
|
|
722
795
|
* @param {import('../../types').AnomalyConfig[]} anomalies
|
|
723
|
-
* @param {number}
|
|
796
|
+
* @param {number} beginUnix - Dataset start (unix seconds)
|
|
724
797
|
* @returns {import('../../types').AnomalyConfig[]}
|
|
725
798
|
*/
|
|
726
|
-
function resolveAnomalies(anomalies,
|
|
799
|
+
function resolveAnomalies(anomalies, beginUnix) {
|
|
727
800
|
if (!Array.isArray(anomalies) || anomalies.length === 0) return null;
|
|
728
|
-
const beginUnix = dayjs.unix(FIXED_NOW).subtract(numDays, 'day').unix();
|
|
729
801
|
return anomalies.map(a => {
|
|
730
802
|
if (!a.type) throw new Error('Each anomaly must have a type');
|
|
731
803
|
if (!a.event) throw new Error('Each anomaly must have an event name');
|
package/lib/core/context.js
CHANGED
|
@@ -156,30 +156,16 @@ export function createContext(config, storage = null, timeConstants = {}) {
|
|
|
156
156
|
return runtime.isBatchMode;
|
|
157
157
|
},
|
|
158
158
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
// Time constants (passed explicitly from index.js; global fallback only for direct test usage)
|
|
171
|
-
FIXED_NOW: timeConstants.fixedNow || global.FIXED_NOW,
|
|
172
|
-
FIXED_BEGIN: timeConstants.fixedBegin || global.FIXED_BEGIN,
|
|
173
|
-
|
|
174
|
-
// PERFORMANCE: Pre-calculated time shift (instead of calculating per-event)
|
|
175
|
-
TIME_SHIFT_SECONDS: (() => {
|
|
176
|
-
const fixedNow = timeConstants.fixedNow || global.FIXED_NOW;
|
|
177
|
-
const actualNow = dayjs().subtract(1, "hour");
|
|
178
|
-
return actualNow.diff(dayjs.unix(fixedNow), "seconds");
|
|
179
|
-
})(),
|
|
180
|
-
|
|
181
|
-
// Max timestamp (unix seconds) — clamp here to prevent future events
|
|
182
|
-
MAX_TIME: dayjs().unix(),
|
|
159
|
+
// Dataset window anchors (resolved by config-validator). FIXED_BEGIN and
|
|
160
|
+
// FIXED_NOW are the authoritative window — events are generated directly
|
|
161
|
+
// inside this range, no time-shift step. DATASET_*_SECONDS are aliases
|
|
162
|
+
// kept for the existing `meta.datasetStart`/`meta.datasetEnd` hook surface.
|
|
163
|
+
// Resolution: explicit timeConstants override, otherwise pull from validated config.
|
|
164
|
+
// validateDungeonConfig always normalizes datasetStart/datasetEnd to unix seconds.
|
|
165
|
+
FIXED_NOW: /** @type {number} */ (timeConstants.fixedNow || config.datasetEnd),
|
|
166
|
+
FIXED_BEGIN: /** @type {number} */ (timeConstants.fixedBegin || config.datasetStart),
|
|
167
|
+
get DATASET_START_SECONDS() { return this.FIXED_BEGIN; },
|
|
168
|
+
get DATASET_END_SECONDS() { return this.FIXED_NOW; },
|
|
183
169
|
};
|
|
184
170
|
|
|
185
171
|
return context;
|
package/lib/core/storage.js
CHANGED
|
@@ -111,7 +111,12 @@ export async function createHookArray(arr = [], opts) {
|
|
|
111
111
|
}
|
|
112
112
|
} else {
|
|
113
113
|
// Slow path for actual transformation hooks
|
|
114
|
-
const allMetaData = {
|
|
114
|
+
const allMetaData = {
|
|
115
|
+
...rest,
|
|
116
|
+
...meta,
|
|
117
|
+
datasetStart: context?.DATASET_START_SECONDS,
|
|
118
|
+
datasetEnd: context?.DATASET_END_SECONDS
|
|
119
|
+
};
|
|
115
120
|
|
|
116
121
|
// Helper to validate events have required properties
|
|
117
122
|
// Note: event-type hooks are handled in the fast path (alreadyHooked),
|
package/lib/generators/events.js
CHANGED
|
@@ -101,19 +101,13 @@ export async function makeEvent(
|
|
|
101
101
|
|
|
102
102
|
// Set event time using TimeSoup for realistic distribution
|
|
103
103
|
if (earliestTime) {
|
|
104
|
-
let
|
|
104
|
+
let unixTime;
|
|
105
105
|
if (isFirstEvent) {
|
|
106
|
-
|
|
106
|
+
unixTime = earliestTime;
|
|
107
107
|
} else {
|
|
108
|
-
|
|
109
|
-
const soupTimestamp = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean, dayOfWeekWeights, hourOfDayWeights, context.TIME_SHIFT_SECONDS);
|
|
110
|
-
shiftedTimestamp = soupTimestamp + context.TIME_SHIFT_SECONDS;
|
|
108
|
+
unixTime = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean, dayOfWeekWeights, hourOfDayWeights);
|
|
111
109
|
}
|
|
112
|
-
|
|
113
|
-
if (shiftedTimestamp > context.MAX_TIME) {
|
|
114
|
-
eventTemplate._drop = true;
|
|
115
|
-
}
|
|
116
|
-
eventTemplate.time = dayjs.unix(Math.min(shiftedTimestamp, context.MAX_TIME)).toISOString();
|
|
110
|
+
eventTemplate.time = dayjs.unix(unixTime).toISOString();
|
|
117
111
|
}
|
|
118
112
|
|
|
119
113
|
// Add anonymous identifiers
|
|
@@ -196,11 +190,12 @@ export async function makeEvent(
|
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
// Perf 1: Compute eventUnix once for all time-based checks
|
|
200
|
-
//
|
|
193
|
+
// Perf 1: Compute eventUnix once for all time-based checks. World events &
|
|
194
|
+
// features were resolved against the dataset window (no shift), and event
|
|
195
|
+
// times now also live in that same window — direct unix conversion works.
|
|
201
196
|
let eventUnix = null;
|
|
202
197
|
if ((worldEventsTimeline || resolvedFeatures) && eventTemplate.time) {
|
|
203
|
-
eventUnix = dayjs(eventTemplate.time).
|
|
198
|
+
eventUnix = dayjs(eventTemplate.time).unix();
|
|
204
199
|
}
|
|
205
200
|
|
|
206
201
|
// Feature 2: World events — inject props for active events at this timestamp
|
|
@@ -303,7 +298,9 @@ export async function makeEvent(
|
|
|
303
298
|
const hookedEvent = await hook(eventTemplate, "event", {
|
|
304
299
|
user: { distinct_id },
|
|
305
300
|
config,
|
|
306
|
-
persona: featureCtx.persona || null
|
|
301
|
+
persona: featureCtx.persona || null,
|
|
302
|
+
datasetStart: context.DATASET_START_SECONDS,
|
|
303
|
+
datasetEnd: context.DATASET_END_SECONDS
|
|
307
304
|
});
|
|
308
305
|
// If hook returns a modified event, use it; otherwise use original
|
|
309
306
|
if (hookedEvent && typeof hookedEvent === 'object') {
|
|
@@ -68,7 +68,11 @@ export async function makeFunnel(context, funnel, user, firstEventTime, profile
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Call pre-funnel hook
|
|
71
|
-
await hook(funnel, "funnel-pre", {
|
|
71
|
+
await hook(funnel, "funnel-pre", {
|
|
72
|
+
user, profile, scd, funnel, config, firstEventTime,
|
|
73
|
+
datasetStart: context.DATASET_START_SECONDS,
|
|
74
|
+
datasetEnd: context.DATASET_END_SECONDS
|
|
75
|
+
});
|
|
72
76
|
|
|
73
77
|
// Extract funnel configuration
|
|
74
78
|
let {
|
|
@@ -206,7 +210,11 @@ export async function makeFunnel(context, funnel, user, firstEventTime, profile
|
|
|
206
210
|
);
|
|
207
211
|
|
|
208
212
|
// Call post-funnel hook
|
|
209
|
-
await hook(finalEvents, "funnel-post", {
|
|
213
|
+
await hook(finalEvents, "funnel-post", {
|
|
214
|
+
user, profile, scd, funnel, config,
|
|
215
|
+
datasetStart: context.DATASET_START_SECONDS,
|
|
216
|
+
datasetEnd: context.DATASET_END_SECONDS
|
|
217
|
+
});
|
|
210
218
|
|
|
211
219
|
return [finalEvents, doesUserConvert];
|
|
212
220
|
}
|
|
@@ -446,8 +454,8 @@ async function generateFunnelEvents(
|
|
|
446
454
|
|
|
447
455
|
try {
|
|
448
456
|
let computedTime = dayjs(funnelStartTime).add(event.relativeTimeMs, "milliseconds");
|
|
449
|
-
// Drop events that would land
|
|
450
|
-
if (context.
|
|
457
|
+
// Drop events that would land past the dataset window
|
|
458
|
+
if (context.FIXED_NOW && computedTime.unix() > context.FIXED_NOW) {
|
|
451
459
|
newEvent._drop = true;
|
|
452
460
|
}
|
|
453
461
|
if (computedTime.isValid()) {
|
package/lib/generators/mirror.js
CHANGED
|
@@ -22,8 +22,9 @@ export async function makeMirror(context) {
|
|
|
22
22
|
if (!mirrorProps || Object.keys(mirrorProps).length === 0) {
|
|
23
23
|
return; // No mirror properties configured
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
// Anchor to the resolved dataset end (not wall-clock) so mirror behavior is deterministic.
|
|
27
|
+
const now = context.FIXED_NOW ? dayjs.unix(context.FIXED_NOW) : dayjs();
|
|
27
28
|
|
|
28
29
|
for (const oldEvent of eventData) {
|
|
29
30
|
let newEvent = null;
|
package/lib/generators/scd.js
CHANGED
|
@@ -48,7 +48,8 @@ export async function makeSCD(context, scdProp, scdKey, distinct_id, mutations,
|
|
|
48
48
|
|
|
49
49
|
const scdEntries = [];
|
|
50
50
|
let lastInserted = dayjs(created);
|
|
51
|
-
|
|
51
|
+
// Anchor to the resolved dataset end (not wall-clock) so SCD count is deterministic.
|
|
52
|
+
const now = context.FIXED_NOW ? dayjs.unix(context.FIXED_NOW) : dayjs();
|
|
52
53
|
const deltaDays = now.diff(lastInserted, "day");
|
|
53
54
|
const uuidKeyName = type === 'user' ? 'distinct_id' : type;
|
|
54
55
|
let lastStartTime = null; // Track for monotonic ordering
|
package/lib/generators/text.js
CHANGED
|
@@ -524,7 +524,7 @@ class OrganicTextGenerator {
|
|
|
524
524
|
intensity: 'medium',
|
|
525
525
|
formality: 'casual',
|
|
526
526
|
min: 100,
|
|
527
|
-
max:
|
|
527
|
+
max: 254, // default capped at Mixpanel string property limit; override per-generator if needed
|
|
528
528
|
seed: null,
|
|
529
529
|
keywords: null,
|
|
530
530
|
keywordDensity: 0.15,
|