@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
  dayjs.extend(utc);
14
14
  import { uid, comma } from 'ak-tools';
15
15
  import { weighNumRange, date, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
16
+ import { scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
16
17
  const chance = initChance(SEED);
17
18
 
18
19
  /** @typedef {import("../../types").Dungeon} Config */
@@ -49,13 +50,27 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
49
50
 
50
51
  /*
51
52
  * ============================================================================
52
- * ANALYTICS HOOKS (7 hooks)
53
- *
54
- * Adds 7. SIGNUP FLOW TIME-TO-CONVERT: gold/platinum loyalty 0.71x faster,
55
- * bronze 1.3x slower (funnel-post). Discover via funnel median time-to-convert
56
- * by loyalty_tier breakdown.
57
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
58
- * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
53
+ * ANALYTICS HOOKS (10 hooks)
54
+ *
55
+ * Adds 7. SIGNUP FLOW TIME-TO-CONVERT: gold/platinum loyalty 0.67x faster,
56
+ * bronze 1.33x slower (everything hook via scaleFunnelTTC on the page-view-
57
+ * to-sign-up window). Loyalty tier is determined from SCD data
58
+ * (meta.scd.loyalty_tier) with deterministic hash fallback. Discover via
59
+ * bound-sequence funnel TTC breakdown by loyalty tier.
60
+ *
61
+ * Adds 8. CHECKOUT FLOW EXPERIMENT: A/B/C experiment on the eCommerce
62
+ * Purchase funnel — "Control", "Express Checkout" (1.25x conversion),
63
+ * "Social Proof" (1.15x conversion, 0.9x TTC). Engine-managed via funnel
64
+ * experiment config; starts 30 days before dataset end.
65
+ *
66
+ * Adds 9. DARK THEME POWER USERS: funnel-pre hook that boosts purchase
67
+ * funnel conversion 1.3x for dark-theme users and penalizes light-theme
68
+ * users to 0.85x. Discoverable by breakdown of funnel conversion by theme.
69
+ *
70
+ * Adds 10. SAVE-ITEM RETENTION: born-in-dataset users who save 2+ items
71
+ * in their first 10 days retain long-term; those below threshold lose ~70%
72
+ * of post-day-25 events. Discoverable by retention or frequency analysis
73
+ * segmented by early save-item count.
59
74
  * ============================================================================
60
75
  *
61
76
  * ----------------------------------------------------------------------------
@@ -200,10 +215,12 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
200
215
  *
201
216
  * PATTERN: Users who view 3-8 items in the dataset window are in the
202
217
  * "considered buyer" sweet spot — every cart item amount and total_value
203
- * gets boosted ~25%. Users who view 9 or more items are over-engaged
204
- * window-shoppers; ~30% of their checkout events are dropped (decision
205
- * paralysis / browse without buy). No flag is stamped — discoverable
206
- * only by binning users on view-item COUNT and comparing avg cart total.
218
+ * gets boosted ~25%, AND ~45% of their add-to-cart events are cloned
219
+ * (with time offsets) to elevate their cart add rate. Users who view
220
+ * 9 or more items are over-engaged window-shoppers; ~30% of their
221
+ * checkout events are dropped (decision paralysis / browse without buy).
222
+ * No flag is stamped — discoverable only by binning users on view-item
223
+ * COUNT and comparing avg cart total or add-to-cart rate.
207
224
  *
208
225
  * HOW TO FIND IT IN MIXPANEL:
209
226
  *
@@ -229,6 +246,121 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
229
246
  * tends to convert at higher cart value; an excessive browser is a
230
247
  * tire-kicker who abandons more often.
231
248
  *
249
+ * ----------------------------------------------------------------------------
250
+ * Hook 7: Signup Flow Time-to-Convert (everything)
251
+ * ----------------------------------------------------------------------------
252
+ *
253
+ * PATTERN: The Signup Flow funnel (page view → view item → save item →
254
+ * page view → sign up) has its time-to-convert scaled by the user's
255
+ * loyalty tier from SCD data (meta.scd.loyalty_tier). Gold and platinum
256
+ * users complete the funnel 0.67x faster; bronze users take 1.33x longer;
257
+ * silver is unaffected. The hook reads the latest SCD entry for
258
+ * loyalty_tier, falling back to a deterministic hash of user_id for
259
+ * consistent tier assignment. It anchors on the first "sign up" event,
260
+ * looks back up to 2 days for all funnel-step events in that window,
261
+ * and scales the whole cluster via scaleFunnelTTC.
262
+ *
263
+ * HOW TO FIND IT IN MIXPANEL:
264
+ *
265
+ * Report 1: Signup Funnel TTC by Loyalty Tier
266
+ * - Report type: Funnels
267
+ * - Steps: "page view" → "view item" → "save item" → "page view" → "sign up"
268
+ * - Measure: Median time to convert
269
+ * - Breakdown: "loyalty_tier" (SCD property)
270
+ * - Expected: gold/platinum ~ 0.67x median TTC vs silver; bronze ~ 1.33x
271
+ *
272
+ * Report 2: Signup Funnel Conversion by Loyalty Tier
273
+ * - Report type: Funnels
274
+ * - Steps: same as above
275
+ * - Breakdown: "loyalty_tier"
276
+ * - Expected: conversion rate similar across tiers (TTC changes, not conversion)
277
+ *
278
+ * REAL-WORLD ANALOGUE: Loyal, high-tier customers already trust the
279
+ * platform and breeze through signup flows, while new or low-engagement
280
+ * users deliberate longer before committing.
281
+ *
282
+ * ----------------------------------------------------------------------------
283
+ * Hook 8: Checkout Flow Experiment (funnel experiment config)
284
+ * ----------------------------------------------------------------------------
285
+ *
286
+ * PATTERN: A/B/C experiment on the eCommerce Purchase funnel. Control
287
+ * group uses base conversion, "Express Checkout" variant gets 1.25x
288
+ * conversion multiplier, "Social Proof" gets 1.15x conversion and 0.9x
289
+ * time-to-convert. Starts 30 days before dataset end.
290
+ *
291
+ * HOW TO FIND IT IN MIXPANEL:
292
+ *
293
+ * Report 1: Experiment Events by Variant
294
+ * - Report type: Insights
295
+ * - Event: "$experiment_started"
296
+ * - Measure: Total
297
+ * - Breakdown: "$experiment_variant"
298
+ * - Expected: roughly equal distribution across Control, Express Checkout, Social Proof
299
+ *
300
+ * Report 2: Purchase Funnel Conversion by Variant
301
+ * - Report type: Funnels
302
+ * - Funnel: view item → add to cart → checkout
303
+ * - Breakdown: "$experiment_variant"
304
+ * - Expected: Express Checkout > Social Proof > Control conversion rate
305
+ *
306
+ * REAL-WORLD ANALOGUE: Product team tests a streamlined checkout and a
307
+ * social proof variant against the existing flow to lift conversion.
308
+ *
309
+ * ----------------------------------------------------------------------------
310
+ * Hook 9: Dark Theme Power Users (funnel-pre)
311
+ * ----------------------------------------------------------------------------
312
+ *
313
+ * PATTERN: Dark-theme users convert 1.3x better on the purchase funnel;
314
+ * light-theme users convert at 0.85x. Custom theme is unaffected.
315
+ *
316
+ * HOW TO FIND IT IN MIXPANEL:
317
+ *
318
+ * Report 1: Purchase Funnel by Theme
319
+ * - Report type: Funnels
320
+ * - Funnel: view item → add to cart → checkout
321
+ * - Breakdown: "theme"
322
+ * - Expected: dark > custom > light conversion rate
323
+ *
324
+ * Report 2: Checkout Count by Theme
325
+ * - Report type: Insights
326
+ * - Event: "checkout"
327
+ * - Measure: Total
328
+ * - Breakdown: "theme"
329
+ * - Expected: dark-theme users over-index on checkout counts
330
+ *
331
+ * REAL-WORLD ANALOGUE: Power users who customize their UI (dark mode)
332
+ * tend to be more committed and convert at higher rates.
333
+ *
334
+ * ----------------------------------------------------------------------------
335
+ * Hook 10: Save-Item Retention (everything — retention magic number)
336
+ * ----------------------------------------------------------------------------
337
+ *
338
+ * PATTERN: Born-in-dataset users who perform 2+ "save item" events in
339
+ * their first 10 days retain long-term. Users below that threshold lose
340
+ * ~70% of events after day 25, simulating churn. No flag is stamped —
341
+ * discoverable only by segmenting users on early save-item count and
342
+ * comparing retention or late-period activity.
343
+ *
344
+ * HOW TO FIND IT IN MIXPANEL:
345
+ *
346
+ * Report 1: Retention by Early Save Count
347
+ * - Report type: Retention
348
+ * - First event: any event
349
+ * - Return event: any event
350
+ * - Cohort A: users with >= 2 "save item" in first 10 days
351
+ * - Cohort B: users with < 2 "save item" in first 10 days
352
+ * - Expected: Cohort A retains at 4+ weeks; Cohort B drops sharply after week 3
353
+ *
354
+ * Report 2: Post-Day-25 Activity
355
+ * - Report type: Insights
356
+ * - Event: any event
357
+ * - Filter: time > first_event + 25 days
358
+ * - Cohort A: >= 2 early saves; Cohort B: < 2 early saves
359
+ * - Expected: Cohort B has ~70% fewer events in the late window
360
+ *
361
+ * REAL-WORLD ANALOGUE: Saving items signals purchase intent and product
362
+ * engagement; users who curate a wishlist early are more likely to return.
363
+ *
232
364
  * ============================================================================
233
365
  * EXPECTED METRICS SUMMARY
234
366
  * ============================================================================
@@ -240,8 +372,17 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
240
372
  * Toys + Shoes Correlation | toys/shoes co-occurrence| ~ 7% | ~ 50%+ | ~ 7x
241
373
  * Quality -> Watch Time | Avg watchTimeSec (240p->2160p) | 0.7x | 1.5x | ~ 2.1x
242
374
  * Item Flattening | category breakdown | nested | flat | n/a
243
- * View-Item Magic Number | sweet (3-8) cart total | 1x | ~ 1.25x | 1.25x
375
+ * View-Item Magic Number | sweet (3-8) cart rate | 1x | ~ 1.45x | 1.45x
244
376
  * View-Item Magic Number | over (9+) checkouts/user| 1x | ~ 0.7x | -30%
377
+ * Signup TTC by Loyalty | gold/plat median TTC | 1x | ~ 0.67x | -33%
378
+ * Signup TTC by Loyalty | bronze median TTC | 1x | ~ 1.33x | +33%
379
+ * Checkout Experiment | Express Checkout conv | 1x | ~ 1.25x | +25%
380
+ * Checkout Experiment | Social Proof conv | 1x | ~ 1.15x | +15%
381
+ * Checkout Experiment | Social Proof TTC | 1x | ~ 0.9x | -10%
382
+ * Dark Theme Power Users | dark-theme conv rate | 1x | ~ 1.3x | +30%
383
+ * Dark Theme Power Users | light-theme conv rate | 1x | ~ 0.85x | -15%
384
+ * Save-Item Retention | post-d25 events (saver) | 1x | ~ 1x | baseline
385
+ * Save-Item Retention | post-d25 events (non-s) | 1x | ~ 0.3x | -70%
245
386
  * ============================================================================
246
387
  */
247
388
 
@@ -457,6 +598,15 @@ const config = {
457
598
  requireRepeats: true,
458
599
  weight: 10,
459
600
  order: "last-fixed",
601
+ experiment: {
602
+ name: "Express Checkout",
603
+ variants: [
604
+ { name: "Control" },
605
+ { name: "Express Checkout", conversionMultiplier: 1.25 },
606
+ { name: "Social Proof", conversionMultiplier: 1.15, ttcMultiplier: 0.9 },
607
+ ],
608
+ startDaysBeforeEnd: 30,
609
+ },
460
610
  }
461
611
 
462
612
  ],
@@ -492,24 +642,15 @@ const config = {
492
642
  lookupTables: [],
493
643
  hook: function (record, type, meta) {
494
644
 
495
- // Hook 7 (T2C): SIGNUP FLOW TIME-TO-CONVERT (funnel-post)
496
- // Gold/platinum loyalty tier users complete the Signup Flow funnel
497
- // 1.4x faster (factor 0.71); bronze users 1.3x slower (factor 1.3).
498
- if (type === "funnel-post") {
499
- const segment = meta?.profile?.loyalty_tier;
500
- if (Array.isArray(record) && record.length > 1) {
501
- const factor = (
502
- segment === "gold" || segment === "platinum" ? 0.71 :
503
- segment === "bronze" ? 1.3 :
504
- 1.0
505
- );
506
- if (factor !== 1.0) {
507
- for (let i = 1; i < record.length; i++) {
508
- const prev = dayjs(record[i - 1].time);
509
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
510
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
511
- }
512
- }
645
+ // H9: Dark Theme Power Users (funnel-pre)
646
+ // Dark-theme users convert 1.3x better on the purchase funnel;
647
+ // light-theme users convert 0.85x. Custom-theme is unchanged.
648
+ if (type === "funnel-pre") {
649
+ const isPurchaseFunnel = meta.funnel?.sequence?.includes("checkout");
650
+ if (isPurchaseFunnel) {
651
+ const theme = meta.profile?.theme;
652
+ if (theme === "dark") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.3));
653
+ else if (theme === "light") record.conversionRate = Math.round(record.conversionRate * 0.85);
513
654
  }
514
655
  }
515
656
 
@@ -614,7 +755,8 @@ const config = {
614
755
  });
615
756
 
616
757
  // Hook 6: View-Item Magic Number (behavioral, no flags)
617
- // Users in 3-8 view-item sweet spot show ~25% higher cart amounts.
758
+ // Users in 3-8 view-item sweet spot show ~25% higher cart amounts
759
+ // AND elevated add-to-cart rate (~45% boost via cloned events).
618
760
  // Users >=9 are over-engaged window-shoppers; ~30% of their checkouts drop.
619
761
  const viewItemCount = record.filter(e => e.event === 'view item').length;
620
762
  if (viewItemCount >= 3 && viewItemCount <= 8) {
@@ -627,6 +769,20 @@ const config = {
627
769
  }));
628
770
  }
629
771
  });
772
+ // Clone ~45% of add-to-cart events to boost cart add rate
773
+ const addToCartEvents = record.filter(e => e.event === 'add to cart');
774
+ const clones = [];
775
+ addToCartEvents.forEach(e => {
776
+ if (chance.bool({ likelihood: 45 })) {
777
+ const offsetMs = integer(60_000, 600_000); // 1-10 min offset
778
+ clones.push({
779
+ ...e,
780
+ insert_id: uid(),
781
+ time: dayjs(e.time).add(offsetMs, 'milliseconds').toISOString()
782
+ });
783
+ }
784
+ });
785
+ if (clones.length > 0) record.push(...clones);
630
786
  } else if (viewItemCount >= 9) {
631
787
  for (let i = record.length - 1; i >= 0; i--) {
632
788
  if (record[i].event === 'checkout' && chance.bool({ likelihood: 30 })) {
@@ -635,6 +791,79 @@ const config = {
635
791
  }
636
792
  }
637
793
 
794
+ // Hook 7 (TTC): SIGNUP FLOW TIME-TO-CONVERT (everything)
795
+ // Gold/platinum loyalty users complete the Signup Flow funnel
796
+ // 0.67x faster; bronze users 1.33x slower. Loyalty tier is read
797
+ // from SCD data (meta.scd.loyalty_tier); falls back to a
798
+ // deterministic hash of user_id for consistent assignment.
799
+ //
800
+ // The funnel uses order:"first-and-last-fixed" so middle steps
801
+ // can reorder. We anchor on the first "sign up" event, look back
802
+ // up to 2 days for all funnel-step events in that window, and
803
+ // scale the whole cluster via scaleFunnelTTC.
804
+ {
805
+ // Determine loyalty tier from SCD (latest entry) or hash fallback
806
+ let loyaltyTier = "silver"; // default baseline
807
+ const scdEntries = meta?.scd?.loyalty_tier;
808
+ if (Array.isArray(scdEntries) && scdEntries.length > 0) {
809
+ // Pick the latest SCD entry
810
+ const latest = scdEntries.reduce((a, b) =>
811
+ (a.time > b.time ? a : b));
812
+ loyaltyTier = latest.loyalty_tier || "silver";
813
+ } else {
814
+ // Deterministic hash fallback: gold ~20%, silver ~30%, bronze ~50%
815
+ const uid = record[0]?.user_id || "";
816
+ const hash = uid.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
817
+ const bucket = hash % 100;
818
+ loyaltyTier = bucket < 20 ? "gold" : bucket < 50 ? "silver" : "bronze";
819
+ }
820
+
821
+ const ttcFactor = (
822
+ loyaltyTier === "gold" || loyaltyTier === "platinum" ? 0.67 :
823
+ loyaltyTier === "bronze" ? 1.33 :
824
+ 1.0
825
+ );
826
+ if (ttcFactor !== 1.0) {
827
+ // Find signup funnel: anchor on first "sign up", look back 2 days
828
+ // for the cluster of funnel-step events
829
+ const funnelSteps = new Set(["page view", "view item", "save item", "sign up"]);
830
+ const sorted = record.slice().sort((a, b) =>
831
+ new Date(a.time) - new Date(b.time));
832
+ const signupEvent = sorted.find(e => e.event === "sign up");
833
+ if (signupEvent) {
834
+ const signupMs = new Date(signupEvent.time).getTime();
835
+ const windowMs = 2 * 24 * 60 * 60 * 1000; // 2-day lookback
836
+ const funnelEvents = sorted.filter(e =>
837
+ funnelSteps.has(e.event) &&
838
+ new Date(e.time).getTime() >= signupMs - windowMs &&
839
+ new Date(e.time).getTime() <= signupMs
840
+ );
841
+ if (funnelEvents.length >= 2) {
842
+ scaleFunnelTTC(funnelEvents, ttcFactor);
843
+ }
844
+ }
845
+ }
846
+ }
847
+
848
+ }
849
+
850
+ // H10: Save-Item Retention (everything — retention magic number)
851
+ // Born-in-dataset users with 2+ "save item" in their first 10 days
852
+ // retain; users below the threshold lose ~70% of events after day 25.
853
+ if (type === "everything" && meta.userIsBornInDataset) {
854
+ const firstT = record[0]?.time;
855
+ if (firstT) {
856
+ const window10 = dayjs(firstT).add(10, 'days').toISOString();
857
+ const saves = record.filter(e => e.event === 'save item' && e.time <= window10).length;
858
+ if (saves < 2) {
859
+ const cutoff = dayjs(firstT).add(25, 'days');
860
+ for (let i = record.length - 1; i >= 0; i--) {
861
+ if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: 70 })) {
862
+ record.splice(i, 1);
863
+ }
864
+ }
865
+ }
866
+ }
638
867
  }
639
868
 
640
869
  // Filter out events tagged for removal in the event hook
@@ -52,7 +52,7 @@ const chance = u.initChance(SEED);
52
52
 
53
53
  /**
54
54
  * ═══════════════════════════════════════════════════════════════════════════════
55
- * ANALYTICS HOOKS (9 hooks)
55
+ * ANALYTICS HOOKS (10 hooks)
56
56
  * ═══════════════════════════════════════════════════════════════════════════════
57
57
  *
58
58
  * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
@@ -216,7 +216,7 @@ const chance = u.initChance(SEED);
216
216
  * ---------------------------------------------------------------
217
217
  * 7. FREE VS PAID COURSES (funnel-pre + everything)
218
218
  *
219
- * PATTERN: Free users get 0.5x funnel conversion rate; paid
219
+ * PATTERN: Free users get 0.5x funnel conversion rate on the cert funnel only; paid
220
220
  * subscribers get 1.5x. Free users also lose 55% of their
221
221
  * certificates, producing a ~2.2x completion gap.
222
222
  *
@@ -266,10 +266,13 @@ const chance = u.initChance(SEED);
266
266
  * despite spending less time.
267
267
  *
268
268
  * ---------------------------------------------------------------
269
- * 9. COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
269
+ * 9. COURSE COMPLETION TIME-TO-CONVERT (everything)
270
270
  *
271
271
  * PATTERN: Annual subscribers complete the course-completion funnel
272
- * 1.4x faster (factor 0.71); Free users 1.4x slower (factor 1.4).
272
+ * 2x faster (factor 0.5); Free users 1.8x slower (factor 1.8).
273
+ * Applied in the everything hook by scaling the enrolled-to-cert
274
+ * gap on the raw events. Stronger factors compensate for the
275
+ * composition effect from H7's conversion-rate gating.
273
276
  *
274
277
  * HOW TO FIND IT IN MIXPANEL:
275
278
  *
@@ -277,15 +280,40 @@ const chance = u.initChance(SEED);
277
280
  * - Funnels > "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
278
281
  * - Measure: Median time to convert
279
282
  * - Breakdown: subscription_status
280
- * - Expected: annual ~ 0.71x baseline; free ~ 1.4x baseline
283
+ * - Expected: annual < monthly < free (direction)
281
284
  *
282
- * NOTE (funnel-post measurement): visible only via Mixpanel funnel
283
- * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
284
- * show this funnel-post adjusts gaps within funnel instances, not
285
- * across the user's full event history.
285
+ * Also visible via cross-event SQL: MIN("course enrolled" time) to
286
+ * MIN("certificate earned" time) per user, broken down by
287
+ * subscription_status. Annual < monthly < free.
286
288
  *
287
289
  * REAL-WORLD ANALOGUE: Paid commitment accelerates throughput.
288
290
  *
291
+ * ---------------------------------------------------------------
292
+ * 10. SOCIAL LEARNING EXPERIMENT (funnel experiment)
293
+ *
294
+ * PATTERN: A/B experiment on the Social Learning funnel (discussion
295
+ * posted → study group joined → resource downloaded). "AI Study
296
+ * Buddy" variant boosts conversion 1.4x and speeds TTC to 0.85x.
297
+ * Activates 30 days before dataset end.
298
+ *
299
+ * HOW TO FIND IT IN MIXPANEL:
300
+ *
301
+ * Report 1: $experiment_started by Variant
302
+ * - Report type: Insights
303
+ * - Event: "$experiment_started"
304
+ * - Measure: Total
305
+ * - Breakdown: "$experiment_name" and "Variant"
306
+ * - Expected: ~50% Control, ~50% AI Study Buddy
307
+ *
308
+ * Report 2: Social Learning Funnel by Variant
309
+ * - Report type: Funnels
310
+ * - Steps: "discussion posted" → "study group joined" → "resource downloaded"
311
+ * - Breakdown: Variant
312
+ * - Expected: AI Study Buddy ~ 1.4x conversion vs Control
313
+ *
314
+ * REAL-WORLD ANALOGUE: AI-powered study companions boost social
315
+ * engagement and resource discovery in cohort-based courses.
316
+ *
289
317
  * ═══════════════════════════════════════════════════════════════════════════════
290
318
  * EXPECTED METRICS SUMMARY
291
319
  * ═══════════════════════════════════════════════════════════════════════════════
@@ -301,7 +329,9 @@ const chance = u.initChance(SEED);
301
329
  * Semester-End Spike | Assessment volume | 1x | ~ 2x | 2x
302
330
  * Free vs Paid | Course completion | 15% | 33% | 2.2x
303
331
  * Playback Speed | Quiz score (speed) | ~ 65 | ~ 73 | +8 pt
304
- * Course Completion T2C | median min by tier | 1x | 0.71x/1.4x | ~ 2x range
332
+ * Course Completion T2C | median min by tier | 1x | 0.5x/1.8x | ~ 3.6x range
333
+ * Social Learning Exp | AI variant conversion | 1x | 1.4x | 1.4x
334
+ * Social Learning Exp | AI variant TTC | 1x | 0.85x | -15%
305
335
  */
306
336
 
307
337
  // Generate consistent IDs for lookup tables and event properties
@@ -374,6 +404,14 @@ const config = {
374
404
  conversionRate: 50,
375
405
  timeToConvert: 12,
376
406
  weight: 2,
407
+ experiment: {
408
+ name: "AI Study Buddy",
409
+ variants: [
410
+ { name: "Control" },
411
+ { name: "AI Study Buddy", conversionMultiplier: 1.4, ttcMultiplier: 0.85 },
412
+ ],
413
+ startDaysBeforeEnd: 30,
414
+ },
377
415
  },
378
416
  {
379
417
  // Instructor interaction loop
@@ -617,7 +655,7 @@ const config = {
617
655
  /**
618
656
  * ARCHITECTED ANALYTICS HOOKS
619
657
  *
620
- * This hook function creates 8 deliberate patterns in the data:
658
+ * This hook function creates 10 deliberate patterns in the data:
621
659
  *
622
660
  * 1. STUDENT VS INSTRUCTOR PROFILES: Instructor profiles get teaching attributes; students get learning attributes
623
661
  * 2. DEADLINE CRAMMING: Assignments submitted on Sun/Mon are rushed and lower quality
@@ -625,8 +663,10 @@ const config = {
625
663
  * 4. STUDY GROUP RETENTION: Early study group joiners retain; non-joiners with low scores churn
626
664
  * 5. HINT DEPENDENCY: Hint users get locked into easy problems; non-hint users tackle harder ones
627
665
  * 6. SEMESTER-END SPIKE: Days 75-85 see doubled assessment activity (cramming period)
628
- * 7. FREE VS PAID COURSES: Paid subscribers convert through Course Completion funnel at ~2.2x rate
666
+ * 7. FREE VS PAID COURSES: Paid subscribers convert through Course Completion funnel at ~2.2x rate (funnel-pre scoped to cert funnel only)
629
667
  * 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
668
+ * 9. COURSE COMPLETION TTC: Annual subscribers complete cert funnel faster; free users slower (everything hook)
669
+ * 10. SOCIAL LEARNING EXPERIMENT: A/B test on Social Learning funnel with AI Study Buddy variant (funnel experiment config)
630
670
  */
631
671
  hook: function (record, type, meta) {
632
672
  // HOOK 1: STUDENT VS INSTRUCTOR PROFILES (user) — role-based attributes.
@@ -664,33 +704,18 @@ const config = {
664
704
  }
665
705
 
666
706
  // HOOK 7 (funnel-pre): FREE VS PAID — free users get 0.5x conversion rate;
667
- // paid subscribers get 1.5x. Applies to course-completion funnel.
707
+ // paid subscribers get 1.5x. Scoped to the course-completion funnel ONLY
708
+ // (sequence ending in "certificate earned") to avoid displacing standalone
709
+ // events for paid users and triggering unintended churn in H4.
668
710
  if (type === "funnel-pre") {
669
- const subStatus = meta?.profile?.subscription_status;
670
- if (subStatus === "free") {
671
- record.conversionRate = Math.round(record.conversionRate * 0.5);
672
- } else if (subStatus === "monthly" || subStatus === "annual") {
673
- record.conversionRate = Math.min(100, Math.round(record.conversionRate * 1.5));
674
- }
675
- }
676
-
677
- // HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
678
- // Annual subscribers complete the Course Completion funnel 1.4x
679
- // faster (factor 0.71); Free users 1.4x slower (factor 1.4).
680
- if (type === "funnel-post") {
681
- const segment = meta?.profile?.subscription_status;
682
- if (Array.isArray(record) && record.length > 1) {
683
- const factor = (
684
- segment === "annual" ? 0.71 :
685
- segment === "free" ? 1.4 :
686
- 1.0
687
- );
688
- if (factor !== 1.0) {
689
- for (let i = 1; i < record.length; i++) {
690
- const prev = dayjs(record[i - 1].time);
691
- const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
692
- record[i].time = prev.add(newGap, "milliseconds").toISOString();
693
- }
711
+ const isCertFunnel = Array.isArray(meta?.funnel?.sequence) &&
712
+ meta.funnel.sequence.includes("certificate earned");
713
+ if (isCertFunnel) {
714
+ const subStatus = meta?.profile?.subscription_status;
715
+ if (subStatus === "free") {
716
+ record.conversionRate = Math.round(record.conversionRate * 0.5);
717
+ } else if (subStatus === "monthly" || subStatus === "annual") {
718
+ record.conversionRate = Math.min(100, Math.round(record.conversionRate * 1.5));
694
719
  }
695
720
  }
696
721
  }
@@ -779,8 +804,51 @@ const config = {
779
804
  });
780
805
  if (duplicates.length > 0) userEvents.push(...duplicates);
781
806
 
782
- // HOOK 7: FREE VS PAID — free users lose 55% of certificates.
783
807
  const subStatus = profile ? profile.subscription_status : "free";
808
+
809
+ // HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (everything)
810
+ // Annual subscribers complete the cert funnel 2x faster (factor 0.5);
811
+ // Free users 1.8x slower (factor 1.8). For each "certificate earned"
812
+ // event, find the nearest preceding "course enrolled" and scale the gap.
813
+ // Runs BEFORE cert-dropping (H7) so TTC adjustments aren't masked by
814
+ // survivorship bias from the 55% free cert removal.
815
+ {
816
+ const ttcFactor = (
817
+ subStatus === "annual" ? 0.5 :
818
+ subStatus === "free" ? 1.8 :
819
+ 1.0
820
+ );
821
+ if (ttcFactor !== 1.0) {
822
+ // Collect all "course enrolled" times (sorted) for binary lookup
823
+ const enrolledTimes = userEvents
824
+ .filter(e => e.event === "course enrolled")
825
+ .map(e => dayjs(e.time))
826
+ .sort((a, b) => a.valueOf() - b.valueOf());
827
+
828
+ if (enrolledTimes.length > 0) {
829
+ for (const event of userEvents) {
830
+ if (event.event === "certificate earned") {
831
+ const certTime = dayjs(event.time);
832
+ // Find the latest enrolled time before this cert
833
+ let anchor = null;
834
+ for (let k = enrolledTimes.length - 1; k >= 0; k--) {
835
+ if (enrolledTimes[k].isBefore(certTime)) {
836
+ anchor = enrolledTimes[k];
837
+ break;
838
+ }
839
+ }
840
+ if (anchor) {
841
+ const gap = certTime.diff(anchor);
842
+ const newGap = Math.round(gap * ttcFactor);
843
+ event.time = anchor.add(newGap, "milliseconds").toISOString();
844
+ }
845
+ }
846
+ }
847
+ }
848
+ }
849
+ }
850
+
851
+ // HOOK 7: FREE VS PAID — free users lose 55% of certificates.
784
852
  if (subStatus === "free") {
785
853
  for (let i = userEvents.length - 1; i >= 0; i--) {
786
854
  if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {