@ak--47/dungeon-master 1.4.2 → 1.4.3

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.
@@ -13,6 +13,7 @@ import utc from "dayjs/plugin/utc.js";
13
13
  import "dotenv/config";
14
14
  import * as u from "../../lib/utils/utils.js";
15
15
  import * as v from "ak-tools";
16
+ import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
16
17
 
17
18
  dayjs.extend(utc);
18
19
  const chance = u.initChance(SEED);
@@ -110,7 +111,7 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
110
111
  * 3. EXPERIENCED DOCTOR SATISFACTION (everything hook)
111
112
  * ───────────────────────────────────────────────────────────────
112
113
  *
113
- * PATTERN: Users who had >50 consultation events get higher avg
114
+ * PATTERN: Users who had >12 consultation events get higher avg
114
115
  * satisfaction_score (boosted to 4.0-5.0 range) vs baseline 1-5.
115
116
  * Simulates experienced doctors earning better reviews.
116
117
  *
@@ -227,23 +228,26 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
227
228
  * and limited scheduling, reducing completed consultations.
228
229
  *
229
230
  * ───────────────────────────────────────────────────────────────
230
- * 9. BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
231
+ * 9. BOOKING FUNNEL TTC BY TIER (everything hook — property scaling)
231
232
  *
232
- * PATTERN: Premium tier completes Booking funnel 1.35x faster
233
- * (factor 0.74); Free tier 1.33x slower (factor 1.33).
233
+ * PATTERN: Premium users get shorter wait times and consultation
234
+ * durations (0.67x); Free users get longer (1.4x); Basic at 1.0x.
235
+ * Scales `wait_time_hours` on "appointment booked" and
236
+ * `duration_minutes` on "consultation completed".
234
237
  *
235
238
  * HOW TO FIND IT IN MIXPANEL:
236
239
  *
237
- * Report 1: Booking Funnel Median Time-to-Convert by Tier
238
- * - Funnels > "symptom search" -> "appointment booked" -> "consultation completed"
239
- * - Measure: Median time to convert
240
+ * Report 1: Wait Time by Subscription Tier
241
+ * - Insights > "appointment booked"
242
+ * - Measure: Average of "wait_time_hours"
240
243
  * - Breakdown: subscription_tier
241
- * - Expected: premium ~ 0.74x; free ~ 1.33x
244
+ * - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
242
245
  *
243
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel
244
- * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
245
- * show this — funnel-post adjusts gaps within funnel instances, not
246
- * across the user's full event history.
246
+ * Report 2: Consultation Duration by Tier
247
+ * - Insights > "consultation completed"
248
+ * - Measure: Average of "duration_minutes"
249
+ * - Breakdown: subscription_tier
250
+ * - Expected: premium ~ 0.67x baseline; free ~ 1.4x baseline
247
251
  *
248
252
  * ───────────────────────────────────────────────────────────────
249
253
  * 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
@@ -279,13 +283,13 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
279
283
  * ----------------------------|---------------------|----------|-----------|------
280
284
  * After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
281
285
  * Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
282
- * Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
286
+ * Experienced Doctor Sat. | satisfaction_score | ~2.0 | 4.0-5.0 | ~2x
283
287
  * Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
284
288
  * Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
285
289
  * Occasional No-Shows | booking→consult | 95% | 75% | -20%
286
290
  * Doctor Specialization | years_experience | 5 | 22 | 4.4x
287
291
  * Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
288
- * Booking T2C | median min by tier | 1x | 0.74/1.33x| ~ 1.8x range
292
+ * Booking TTC by Tier | wait_time/duration | 1x | 0.67/1.4x | ~ 2.1x range
289
293
  * Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
290
294
  * Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
291
295
  */
@@ -706,27 +710,6 @@ const config = {
706
710
  }
707
711
  }
708
712
 
709
- // HOOK 9 (T2C): BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
710
- // Premium tier completes Booking funnel 1.35x faster (factor 0.74);
711
- // Free tier 1.33x slower (factor 1.33).
712
- if (type === "funnel-post") {
713
- const segment = meta?.profile?.subscription_tier;
714
- if (Array.isArray(record) && record.length > 1) {
715
- const factor = (
716
- segment === "premium" ? 0.74 :
717
- segment === "free" ? 1.33 :
718
- 1.0
719
- );
720
- if (factor !== 1.0) {
721
- for (let i = 1; i < record.length; i++) {
722
- const prev = dayjs(record[i - 1].time);
723
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
724
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
725
- }
726
- }
727
- }
728
- }
729
-
730
713
  // (HOOK 1: AFTER-HOURS SURGE PRICING moved to everything hook — hour checks
731
714
  // must run after bunchIntoSessions redistributes timestamps)
732
715
  // (HOOK 2: FLU SEASON SPIKE moved to everything hook — same reason)
@@ -750,6 +733,33 @@ const config = {
750
733
  });
751
734
  }
752
735
 
736
+ // HOOK 9: BOOKING FUNNEL TTC BY TIER (property scaling)
737
+ // Premium users get shorter wait_time_hours (0.67x) and duration_minutes (0.67x).
738
+ // Free users get longer wait_time_hours (1.4x) and duration_minutes (1.4x).
739
+ // Basic users stay at baseline. SQL-measurable via AVG(wait_time_hours) broken by tier.
740
+ if (profile) {
741
+ const userTier = profile.subscription_tier;
742
+ const ttcFactor = userTier === "premium" ? 0.67 : userTier === "free" ? 1.4 : 1.0;
743
+ if (ttcFactor !== 1.0) {
744
+ // Timestamp shift: affects Mixpanel funnel TTC
745
+ const bookingSeq = findFirstSequence(
746
+ record,
747
+ ["appointment booked", "consultation completed", "follow up scheduled"],
748
+ 60 * 24 * 30
749
+ );
750
+ if (bookingSeq) scaleFunnelTTC(bookingSeq, ttcFactor);
751
+ // Property scale: affects Insights AVG reports
752
+ record.forEach(e => {
753
+ if (e.event === "appointment booked" && typeof e.wait_time_hours === "number") {
754
+ e.wait_time_hours = Math.round(e.wait_time_hours * ttcFactor * 10) / 10;
755
+ }
756
+ if (e.event === "consultation completed" && typeof e.duration_minutes === "number") {
757
+ e.duration_minutes = Math.round(e.duration_minutes * ttcFactor);
758
+ }
759
+ });
760
+ }
761
+ }
762
+
753
763
  // HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
754
764
  // UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
755
765
  record.forEach(e => {
@@ -783,13 +793,13 @@ const config = {
783
793
  }
784
794
 
785
795
  // ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
786
- // Users with >50 consultation events get boosted satisfaction scores.
796
+ // Users with >12 consultation events get boosted satisfaction scores.
787
797
  let consultCount = 0;
788
798
  record.forEach(e => {
789
799
  if (e.event === "consultation completed") consultCount++;
790
800
  });
791
801
 
792
- if (consultCount > 50) {
802
+ if (consultCount > 12) {
793
803
  record.forEach(e => {
794
804
  if (e.event === "consultation completed") {
795
805
  e.satisfaction_score = chance.floating({ min: 4.0, max: 5.0, fixed: 1 });
@@ -31,6 +31,9 @@ const chance = u.initChance(SEED);
31
31
  *
32
32
  * - 5,000 users over 100 days
33
33
  * - 600,000 events across 18 event types
34
+ * - 10 hooks (version stamping, ticket volume, conversion boost, magic number,
35
+ * TTC scaling, claims experiment, risk approval, doc upload retention,
36
+ * renewal spike, claim-to-premium)
34
37
  * - 5 funnels (onboarding, application, approval, claims, renewal)
35
38
  * - 5 insurance types as super property (auto, home, life, health, renters)
36
39
  * - Deterministic app versioning (2.10 → 2.11 → 2.12 → 2.13)
@@ -52,12 +55,13 @@ const chance = u.initChance(SEED);
52
55
 
53
56
  /*
54
57
  * ===================================================================
55
- * ANALYTICS HOOKS (5 hooks)
58
+ * ANALYTICS HOOKS (10 hooks)
56
59
  *
57
- * Adds 5. APPLICATION COMPLETION TIME-TO-CONVERT: business 0.74x faster,
58
- * family 1.3x slower (funnel-post). Discover via funnel median TTC by account_type.
59
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
60
- * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
60
+ * Hooks 1-5: Version stamping, support ticket volume drop, application
61
+ * conversion boost, step-count magic number, application TTC by account type.
62
+ *
63
+ * Hooks 6-10: Claims experiment, risk profile approval gating, document
64
+ * upload retention, end-of-quarter renewal spike, claim-to-premium increase.
61
65
  * ===================================================================
62
66
  *
63
67
  * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
@@ -169,6 +173,150 @@ const chance = u.initChance(SEED);
169
173
  * REAL-WORLD ANALOGUE: Engaged applicants get higher premiums approved;
170
174
  * over-engaged ones look like fraud and get flagged.
171
175
  *
176
+ * -------------------------------------------------------------------
177
+ * 5. APPLICATION COMPLETION TIME-TO-CONVERT (everything)
178
+ * -------------------------------------------------------------------
179
+ *
180
+ * PATTERN: In the everything hook, place each "application approved"
181
+ * event at a fixed target offset from the first "application started"
182
+ * event, determined by account_type (assigned via deterministic hash):
183
+ * - business: ~36h target offset (~0.75x baseline)
184
+ * - individual: ~48h target offset (baseline)
185
+ * - family: ~63h target offset (~1.31x baseline)
186
+ *
187
+ * HOW TO FIND IT IN MIXPANEL:
188
+ *
189
+ * Report 1: Application Funnel TTC by Account Type
190
+ * - Report type: Funnels
191
+ * - Steps: "application started" → "application approved"
192
+ * - Breakdown: account_type (from account created event)
193
+ * - Measure: Median time to convert
194
+ * - Expected: business < individual < family
195
+ *
196
+ * REAL-WORLD ANALOGUE: Business applicants have streamlined processes
197
+ * and pre-filled forms; family policies require more documentation.
198
+ *
199
+ * -------------------------------------------------------------------
200
+ * 6. CLAIMS PROCESS EXPERIMENT (funnel config — no hook code)
201
+ * -------------------------------------------------------------------
202
+ *
203
+ * PATTERN: A/B experiment on the Claims Process funnel. The engine
204
+ * fires $experiment_started events and applies conversion/TTC
205
+ * multipliers automatically. "Simplified Claims" variant gets 1.3x
206
+ * conversion and 0.8x time-to-convert. Experiment runs in the last
207
+ * 35 days of the dataset.
208
+ *
209
+ * HOW TO FIND IT IN MIXPANEL:
210
+ *
211
+ * Report 1: Experiment Results
212
+ * - Report type: Funnels
213
+ * - Steps: "claim filed" -> "claim status checked" -> "support ticket created"
214
+ * - Breakdown: "Variant name"
215
+ * - Date range: last 35 days of dataset
216
+ * - Expected: "Simplified Claims" has ~1.3x conversion vs "Control"
217
+ *
218
+ * Report 2: Experiment Started Events
219
+ * - Report type: Insights
220
+ * - Event: "$experiment_started"
221
+ * - Breakdown: "Variant name"
222
+ * - Expected: roughly equal distribution between Control and Simplified Claims
223
+ *
224
+ * REAL-WORLD ANALOGUE: A/B testing a simplified claims flow to reduce
225
+ * friction and improve completion rates.
226
+ *
227
+ * -------------------------------------------------------------------
228
+ * 7. RISK PROFILE AFFECTS APPROVAL (funnel-pre)
229
+ * -------------------------------------------------------------------
230
+ *
231
+ * PATTERN: In the funnel-pre hook, low-risk users get 1.8x approval
232
+ * funnel conversion (capped at 95%), high-risk users get 0.3x.
233
+ * Medium-risk users are unchanged.
234
+ *
235
+ * HOW TO FIND IT IN MIXPANEL:
236
+ *
237
+ * Report 1: Application Approval Funnel by Risk Profile
238
+ * - Report type: Funnels
239
+ * - Steps: "application submitted" -> "application approved" -> "policy activated"
240
+ * - Breakdown: "risk_profile" (user property)
241
+ * - Expected: low >> medium >> high conversion rates
242
+ *
243
+ * REAL-WORLD ANALOGUE: Underwriting engines auto-approve low-risk
244
+ * applicants and require manual review for high-risk ones.
245
+ *
246
+ * -------------------------------------------------------------------
247
+ * 8. DOCUMENT UPLOAD RETENTION (everything — retention magic number)
248
+ * -------------------------------------------------------------------
249
+ *
250
+ * PATTERN: Users who upload 3+ documents in their first 14 days
251
+ * retain normally. Users with fewer uploads lose 75% of events after
252
+ * day 30. Only affects born-in-dataset users.
253
+ *
254
+ * HOW TO FIND IT IN MIXPANEL:
255
+ *
256
+ * Report 1: Retention by Document Upload Cohort
257
+ * - Report type: Retention
258
+ * - Starting event: "account created"
259
+ * - Return event: any event
260
+ * - Cohort A: users with >= 3 "document uploaded" in first 14 days
261
+ * - Cohort B: users with < 3 "document uploaded" in first 14 days
262
+ * - Expected: Cohort A retains well past day 30; Cohort B drops off sharply
263
+ *
264
+ * Report 2: Post-Day-30 Event Volume
265
+ * - Report type: Insights
266
+ * - Event: all events
267
+ * - Compare doc-uploader cohort vs non-uploader
268
+ * - Expected: non-uploaders have ~75% fewer events after day 30
269
+ *
270
+ * REAL-WORLD ANALOGUE: Users who complete onboarding paperwork early
271
+ * are invested in the product and retain; those who don't churn.
272
+ *
273
+ * -------------------------------------------------------------------
274
+ * 9. END-OF-QUARTER RENEWAL SPIKE (everything — temporal)
275
+ * -------------------------------------------------------------------
276
+ *
277
+ * PATTERN: Days 85-95 of the dataset get 3x renewal completed events
278
+ * and 2x coverage reviewed events via cloning. Simulates end-of-quarter
279
+ * policy renewal batch processing.
280
+ *
281
+ * HOW TO FIND IT IN MIXPANEL:
282
+ *
283
+ * Report 1: Renewal Volume Over Time
284
+ * - Report type: Insights
285
+ * - Event: "renewal completed"
286
+ * - Measure: Total
287
+ * - Line chart by day
288
+ * - Expected: visible spike at days 85-95, ~3x baseline volume
289
+ *
290
+ * Report 2: Coverage Review Spike
291
+ * - Report type: Insights
292
+ * - Event: "coverage reviewed"
293
+ * - Line chart by day
294
+ * - Expected: ~2x volume during days 85-95
295
+ *
296
+ * REAL-WORLD ANALOGUE: Insurance companies batch-process renewals at
297
+ * quarter-end, producing predictable volume spikes.
298
+ *
299
+ * -------------------------------------------------------------------
300
+ * 10. CLAIM-TO-PREMIUM INCREASE (event — closure Map)
301
+ * -------------------------------------------------------------------
302
+ *
303
+ * PATTERN: After a user files a claim, their next "payment made" event
304
+ * gets premium_amount doubled (2.0x). Uses a module-level Map to
305
+ * track state across events. The Map entry is consumed (deleted) on
306
+ * the first payment after the claim, so the effect is one-shot.
307
+ *
308
+ * HOW TO FIND IT IN MIXPANEL:
309
+ *
310
+ * Report 1: Average Premium After Claim
311
+ * - Report type: Insights
312
+ * - Event: "payment made"
313
+ * - Measure: Average of "premium_amount"
314
+ * - Cohort A: users who did "claim filed" before
315
+ * - Cohort B: users who never filed a claim
316
+ * - Expected: Cohort A premium_amount ~ 2.0x Cohort B
317
+ *
318
+ * REAL-WORLD ANALOGUE: Insurance premiums increase after filing a claim.
319
+ *
172
320
  * ===================================================================
173
321
  * EXPECTED METRICS SUMMARY
174
322
  * ===================================================================
@@ -180,8 +328,20 @@ const chance = u.initChance(SEED);
180
328
  * Application Conversion | approval rate pre/post | 1x | ~ 1.3x | step-up
181
329
  * Step-Count Magic Number | sweet approved_premium | 1x | 1.35x | 1.35x
182
330
  * Step-Count Magic Number | over approvals/user | 1x | 0.6x | -40%
331
+ * Application TTC | business vs individual | 1x | 0.74x | faster
332
+ * Application TTC | family vs individual | 1x | 1.3x | slower
333
+ * Claims Experiment | simplified vs control | 1x | 1.3x | +30%
334
+ * Risk Profile Approval | low vs medium conv rate | 1x | 1.8x | +80%
335
+ * Risk Profile Approval | high vs medium conv rate| 1x | 0.3x | -70%
336
+ * Document Upload Ret. | post-d30 non-uploaders | 1x | 0.25x | -75%
337
+ * Renewal Spike | renewals d85-95 vs base | 1x | 3x | +200%
338
+ * Renewal Spike | reviews d85-95 vs base | 1x | 2x | +100%
339
+ * Claim-to-Premium | premium after claim | 1x | 2.0x | +100%
183
340
  */
184
341
 
342
+ // ── H10 closure state: tracks users who filed a claim ──
343
+ const claimFilers = new Map();
344
+
185
345
  /** @type {Config} */
186
346
  const config = {
187
347
  version: 2,
@@ -364,6 +524,7 @@ const config = {
364
524
  properties: {
365
525
  payment_method: ["credit_card", "bank_transfer", "auto_pay", "check"],
366
526
  amount: u.weighNumRange(30, 800, 0.3, 150),
527
+ premium_amount: u.weighNumRange(50, 600, 0.3, 150),
367
528
  payment_status: ["success", "success", "success", "success", "failed"],
368
529
  },
369
530
  },
@@ -477,6 +638,14 @@ const config = {
477
638
  conversionRate: 50,
478
639
  timeToConvert: 24,
479
640
  weight: 2,
641
+ experiment: {
642
+ name: "Simplified Claims Flow",
643
+ variants: [
644
+ { name: "Control" },
645
+ { name: "Simplified Claims", conversionMultiplier: 1.3, ttcMultiplier: 0.8 },
646
+ ],
647
+ startDaysBeforeEnd: 35,
648
+ },
480
649
  },
481
650
  {
482
651
  name: "Policy Renewal",
@@ -508,53 +677,50 @@ const config = {
508
677
 
509
678
  // ── Hook Function ──
510
679
  /**
511
- * ARCHITECTED ANALYTICS HOOKS
512
- *
513
- * This hook function creates 3 deliberate patterns in the data:
514
- *
515
- * 1. VERSION STAMPING (everything): Every event gets a deterministic app_version
516
- * based on its final timestamp. v2.10 → v2.11 → v2.12 → v2.13.
517
- * All users shift simultaneously on release dates. Uses the everything
518
- * hook (not event hook) because funnel events adjust time after the
519
- * event hook runs.
680
+ * ARCHITECTED ANALYTICS HOOKS (10 total)
520
681
  *
521
- * 2. SUPPORT TICKET VOLUME (everything): Pre-v2.13 period has inflated
522
- * support ticket volume (2-3 extra tickets per user with bug-related
523
- * categories). Post-v2.13, tickets are progressively removed — creating
524
- * a clear volume drop that trends downward.
682
+ * This hook function creates 10 deliberate patterns in the data:
525
683
  *
526
- * 3. APPLICATION CONVERSION BOOST (everything): Pre-v2.13, ~40% of
527
- * application approved and policy activated events are removed,
528
- * lowering the effective funnel conversion rate. Post-v2.13 events
529
- * are left intact, making the conversion visibly jump up.
684
+ * 1. VERSION STAMPING (everything): Deterministic app_version based on timestamp.
685
+ * 2. SUPPORT TICKET VOLUME (everything): Inflated pre-v2.13, progressive drop post.
686
+ * 3. APPLICATION CONVERSION BOOST (everything): Pre-v2.13 approval drop.
687
+ * 4. STEP-COUNT MAGIC NUMBER (everything): 8-14 steps = +35% premium; 15+ = -40% approvals.
688
+ * 5. APPLICATION TTC (everything): Business 0.74x faster, family 1.3x slower.
689
+ * 6. CLAIMS EXPERIMENT (funnel config): A/B test on claims funnel (engine-handled).
690
+ * 7. RISK PROFILE APPROVAL (funnel-pre): Low-risk 1.8x, high-risk 0.3x conversion.
691
+ * 8. DOCUMENT UPLOAD RETENTION (everything): 3+ uploads in 14d = retain; else -75% post-d30.
692
+ * 9. RENEWAL SPIKE (everything): Days 85-95 get 3x renewals, 2x coverage reviews.
693
+ * 10. CLAIM-TO-PREMIUM (event): Premium 2.0x on first payment after claim filed.
530
694
  */
531
695
  hook: function (record, type, meta) {
532
- // HOOK 5 (T2C): APPLICATION COMPLETION TIME-TO-CONVERT (funnel-post)
533
- // Business accounts complete the Application Completion funnel 1.35x
534
- // faster (factor 0.74); family accounts 1.3x slower (factor 1.3).
535
- if (type === "funnel-post") {
536
- const profile = meta?.profile;
537
- const accountType = profile && profile.account_type;
538
- if (Array.isArray(record) && record.length > 1) {
539
- const factor = (
540
- accountType === "business" ? 0.74 :
541
- accountType === "family" ? 1.3 :
542
- 1.0
543
- );
544
- if (factor !== 1.0) {
545
- for (let i = 1; i < record.length; i++) {
546
- const prev = dayjs(record[i - 1].time);
547
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
548
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
549
- }
550
- }
696
+ // =============================================================
697
+ // H7: RISK PROFILE AFFECTS APPROVAL (funnel-pre)
698
+ // =============================================================
699
+ if (type === "funnel-pre") {
700
+ const isApprovalFunnel = meta.funnel?.sequence?.includes("application approved");
701
+ if (isApprovalFunnel) {
702
+ const risk = meta.profile?.risk_profile;
703
+ if (risk === "low") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.8));
704
+ else if (risk === "high") record.conversionRate = Math.round(record.conversionRate * 0.3);
551
705
  }
552
706
  }
553
707
 
554
708
  // =============================================================
555
- // Hooks #1-#3 all run in the "everything" hook so that version
556
- // stamping sees final timestamps (funnel events adjust time
557
- // AFTER the event hook runs).
709
+ // H10: CLAIM-TO-PREMIUM INCREASE (event closure Map)
710
+ // =============================================================
711
+ if (type === "event") {
712
+ if (record.event === "claim filed") {
713
+ claimFilers.set(record.user_id, true);
714
+ }
715
+ if (record.event === "payment made" && claimFilers.has(record.user_id)) {
716
+ record.premium_amount = Math.round((record.premium_amount || 500) * 2.0);
717
+ claimFilers.delete(record.user_id);
718
+ }
719
+ }
720
+
721
+ // =============================================================
722
+ // Everything hooks: H5 first (timestamps), then H2-H4, then
723
+ // H8-H9, then H1 (version stamping LAST).
558
724
  // =============================================================
559
725
  if (type === "everything") {
560
726
  const datasetStart = dayjs.unix(meta.datasetStart);
@@ -573,27 +739,61 @@ const config = {
573
739
  e.app_version = profile.app_version;
574
740
  });
575
741
 
576
- // ─── Hook #1: VERSION STAMPING ───
577
- // Deterministic app_version on every event based on its final timestamp.
578
- // v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
579
- for (const evt of userEvents) {
580
- const eventTime = dayjs(evt.time);
581
- if (eventTime.isBefore(V211_DATE)) {
582
- evt.app_version = "2.10";
583
- } else if (eventTime.isBefore(V212_DATE)) {
584
- evt.app_version = "2.11";
585
- } else if (eventTime.isBefore(V213_DATE)) {
586
- evt.app_version = "2.12";
587
- } else {
588
- evt.app_version = "2.13";
589
- }
590
- }
591
-
592
742
  // Find a user_id from any existing event
593
743
  const userId =
594
744
  userEvents.find((e) => e.user_id)?.user_id ||
595
745
  userEvents[0]?.device_id;
596
746
 
747
+ // ─── Hook #5: APPLICATION COMPLETION TIME-TO-CONVERT ───
748
+ // Runs FIRST: adjusts timestamps before version stamping.
749
+ // Business accounts complete the application funnel faster (0.74x),
750
+ // family accounts slower (1.3x). Individual stays at baseline.
751
+ // Assigns account_type deterministically per user via djb2 hash
752
+ // (independent of engine RNG). Places each "application approved"
753
+ // event at a fixed offset from the first "application started".
754
+ {
755
+ // djb2 hash of userId → deterministic cohort
756
+ let h = 5381;
757
+ for (let i = 0; i < userId.length; i++) {
758
+ h = ((h << 5) + h + userId.charCodeAt(i)) | 0;
759
+ }
760
+ const acctTypes = ["individual", "family", "business"];
761
+ const userAccountType = acctTypes[((h % 3) + 3) % 3];
762
+ // Stamp account created events with the deterministic type
763
+ for (const evt of userEvents) {
764
+ if (evt.event === "account created") {
765
+ evt.account_type = userAccountType;
766
+ }
767
+ }
768
+ userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
769
+ // Collect all "application started" times
770
+ const startedTimes = [];
771
+ for (const evt of userEvents) {
772
+ if (evt.event === "application started") {
773
+ startedTimes.push(dayjs(evt.time).valueOf());
774
+ }
775
+ }
776
+ if (startedTimes.length > 0) {
777
+ const firstStartTime = startedTimes[0];
778
+ // Target hours: biz=36, indiv=48, family=63
779
+ // Ratios: biz/indiv≈0.75, family/indiv≈1.31
780
+ const targetHours = (
781
+ userAccountType === "business" ? 36 :
782
+ userAccountType === "family" ? 63 :
783
+ 48
784
+ );
785
+ const targetMs = targetHours * 3600000;
786
+ for (const evt of userEvents) {
787
+ if (evt.event !== "application approved") continue;
788
+ const evtTime = dayjs(evt.time).valueOf();
789
+ const origGap = evtTime - firstStartTime;
790
+ // Small jitter from original gap (mod 4h) for variance
791
+ const jitter = origGap > 0 ? (origGap % (4 * 3600000)) : 0;
792
+ evt.time = dayjs(firstStartTime + targetMs + jitter).toISOString();
793
+ }
794
+ }
795
+ }
796
+
597
797
  // ─── Hook #2: SUPPORT TICKET VOLUME ───
598
798
  // PRE-V2.13: Inject 2-3 extra support tickets with bug-related categories
599
799
  const preV213Tickets = userEvents.filter(
@@ -715,7 +915,88 @@ const config = {
715
915
  }
716
916
  }
717
917
 
718
- // Sort events by time after injection/removal
918
+ // ─── Hook #8: DOCUMENT UPLOAD RETENTION ───
919
+ // Users with 3+ "document uploaded" in first 14 days retain;
920
+ // others lose 75% of events post-day-30.
921
+ if (meta.userIsBornInDataset) {
922
+ const firstT = userEvents[0]?.time;
923
+ if (firstT) {
924
+ const window14 = dayjs(firstT).add(14, "days").toISOString();
925
+ const docUploads = userEvents.filter(
926
+ (e) => e.event === "document uploaded" && e.time <= window14
927
+ ).length;
928
+ if (docUploads < 3) {
929
+ const cutoff = dayjs(firstT).add(30, "days");
930
+ for (let i = userEvents.length - 1; i >= 0; i--) {
931
+ if (
932
+ dayjs(userEvents[i].time).isAfter(cutoff) &&
933
+ chance.bool({ likelihood: 75 })
934
+ ) {
935
+ userEvents.splice(i, 1);
936
+ }
937
+ }
938
+ }
939
+ }
940
+ }
941
+
942
+ // ─── Hook #9: END-OF-QUARTER RENEWAL SPIKE ───
943
+ // Days 85-95 get 3x renewal clones + 2x coverage reviewed clones
944
+ {
945
+ const spikeStart = datasetStart.add(85, "days");
946
+ const spikeEnd = datasetStart.add(95, "days");
947
+ const clones = [];
948
+ userEvents.forEach((e) => {
949
+ const t = dayjs(e.time);
950
+ if (t.isAfter(spikeStart) && t.isBefore(spikeEnd)) {
951
+ if (e.event === "renewal completed") {
952
+ for (let c = 0; c < 2; c++) {
953
+ clones.push({
954
+ ...e,
955
+ time: t
956
+ .add(
957
+ chance.integer({ min: 5, max: 240 }),
958
+ "minutes"
959
+ )
960
+ .toISOString(),
961
+ insert_id: chance.guid(),
962
+ });
963
+ }
964
+ }
965
+ if (e.event === "coverage reviewed") {
966
+ clones.push({
967
+ ...e,
968
+ time: t
969
+ .add(
970
+ chance.integer({ min: 5, max: 120 }),
971
+ "minutes"
972
+ )
973
+ .toISOString(),
974
+ insert_id: chance.guid(),
975
+ });
976
+ }
977
+ }
978
+ });
979
+ if (clones.length) userEvents.push(...clones);
980
+ }
981
+
982
+ // ─── Hook #1: VERSION STAMPING ───
983
+ // Runs LAST: stamps app_version based on FINAL timestamps
984
+ // (after Hook #5 TTC scaling has adjusted event times).
985
+ // v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
986
+ for (const evt of userEvents) {
987
+ const eventTime = dayjs(evt.time);
988
+ if (eventTime.isBefore(V211_DATE)) {
989
+ evt.app_version = "2.10";
990
+ } else if (eventTime.isBefore(V212_DATE)) {
991
+ evt.app_version = "2.11";
992
+ } else if (eventTime.isBefore(V213_DATE)) {
993
+ evt.app_version = "2.12";
994
+ } else {
995
+ evt.app_version = "2.13";
996
+ }
997
+ }
998
+
999
+ // Sort events by time after injection/removal/shifting
719
1000
  userEvents.sort(
720
1001
  (a, b) => new Date(a.time) - new Date(b.time)
721
1002
  );