@ak--47/dungeon-master 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/hook-helpers-verify.js +89 -0
  3. package/dungeons/technical/identity-model-verify.js +47 -0
  4. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  5. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  6. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  7. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  8. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  9. package/dungeons/vertical/ai-platform.js +45 -52
  10. package/dungeons/vertical/community.js +11 -8
  11. package/dungeons/vertical/crypto.js +25 -24
  12. package/dungeons/vertical/dating.js +56 -48
  13. package/dungeons/vertical/devtools.js +25 -18
  14. package/dungeons/vertical/ecommerce.js +42 -38
  15. package/dungeons/vertical/education.js +24 -9
  16. package/dungeons/vertical/fintech.js +13 -8
  17. package/dungeons/vertical/fitness.js +73 -122
  18. package/dungeons/vertical/food-delivery.js +18 -19
  19. package/dungeons/vertical/gaming.js +19 -20
  20. package/dungeons/vertical/healthcare.js +11 -8
  21. package/dungeons/vertical/insurance-application.js +6 -3
  22. package/dungeons/vertical/logistics.js +15 -9
  23. package/dungeons/vertical/marketplace.js +36 -27
  24. package/dungeons/vertical/media.js +27 -25
  25. package/dungeons/vertical/real-estate.js +18 -7
  26. package/dungeons/vertical/sass.js +84 -68
  27. package/dungeons/vertical/social.js +46 -47
  28. package/dungeons/vertical/travel.js +8 -5
  29. package/lib/core/config-validator.js +136 -157
  30. package/lib/generators/events.js +49 -93
  31. package/lib/generators/funnels.js +202 -91
  32. package/lib/hook-helpers/_internal.js +23 -0
  33. package/lib/hook-helpers/cohort.js +124 -0
  34. package/lib/hook-helpers/identity.js +56 -0
  35. package/lib/hook-helpers/index.js +44 -0
  36. package/lib/hook-helpers/inject.js +99 -0
  37. package/lib/hook-helpers/mutate.js +151 -0
  38. package/lib/hook-helpers/timing.js +99 -0
  39. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  40. package/lib/hook-patterns/attributed-by-source.js +72 -0
  41. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  42. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  43. package/lib/hook-patterns/index.js +14 -0
  44. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  45. package/lib/orchestrators/user-loop.js +119 -269
  46. package/lib/utils/utils.js +29 -16
  47. package/lib/verify/emulate-breakdown.js +281 -0
  48. package/lib/verify/index.js +12 -0
  49. package/lib/verify/verify-dungeon.js +61 -0
  50. package/package.json +6 -4
  51. package/types.d.ts +397 -211
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pattern: Frequency × Frequency.
3
+ *
4
+ * Engineers the joint distribution of `count(metricEvent)` × `count(cohortEvent)`
5
+ * per user, so Mixpanel's "Frequency Distribution of A by per-user count of B"
6
+ * Insights view shows a deliberate shape (e.g., users with 5–20 cohort events
7
+ * have 2x the metric event count of users with <5).
8
+ *
9
+ * Mechanism: classify the user into a bin based on their `cohortEvent` count,
10
+ * then `scaleEventCount(events, targetEvent, multipliers[bin])` to scale that
11
+ * user's count of the target event up or down.
12
+ *
13
+ * Identity & schema constraints:
14
+ * - Operates on the user's full event stream — call from the `everything` hook.
15
+ * - Does NOT add new properties; uses existing event names defined in the dungeon
16
+ * schema.
17
+ * - Cloned events have their `insert_id` stripped (mutate.scaleEventCount handles
18
+ * that), so the engine's batch writer can re-stamp them downstream.
19
+ */
20
+
21
+ import { binUsersByEventCount } from '../hook-helpers/cohort.js';
22
+ import { scaleEventCount } from '../hook-helpers/mutate.js';
23
+
24
+ /**
25
+ * @param {Array<Object>} events - User's event stream (mutated in place).
26
+ * @param {Object} _profile - User profile (unused, kept for API symmetry).
27
+ * @param {Object} opts
28
+ * @param {string} opts.cohortEvent - Event whose per-user count classifies the user.
29
+ * @param {Record<string, [number, number]>} opts.bins - Bin name → [lo, hi).
30
+ * @param {string} opts.targetEvent - Event whose count is scaled per bin.
31
+ * @param {Record<string, number>} opts.multipliers - Bin name → multiplier (1 = no-op,
32
+ * 2 = double, 0.5 = halve). Bins absent from this map use multiplier 1.
33
+ * @returns {{ bin: string|null, delta: number }} Bin assigned + signed delta from
34
+ * scaleEventCount (positive = clones added; negative = events dropped).
35
+ */
36
+ export function applyFrequencyByFrequency(events, _profile, { cohortEvent, bins, targetEvent, multipliers }) {
37
+ if (!events || !cohortEvent || !bins || !targetEvent || !multipliers) {
38
+ return { bin: null, delta: 0 };
39
+ }
40
+ const bin = binUsersByEventCount(events, cohortEvent, bins);
41
+ if (!bin) return { bin: null, delta: 0 };
42
+ const factor = multipliers[bin];
43
+ if (typeof factor !== 'number' || factor === 1) return { bin, delta: 0 };
44
+ const delta = scaleEventCount(events, targetEvent, factor);
45
+ return { bin, delta };
46
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Pattern: Funnel Frequency Breakdown.
3
+ *
4
+ * Inside a `funnel-post` hook, vary the user's completion of the funnel by their
5
+ * count of `cohortEvent` (anywhere in the dataset, not just the funnel). Used
6
+ * when you want Mixpanel's funnel report — broken down by per-user count of an
7
+ * activity event — to show e.g. "users who did 5+ X are 1.4x as likely to
8
+ * complete this funnel."
9
+ *
10
+ * Mechanism: for users in a "drop-prone" bin, drop the funnel's final step
11
+ * event(s) per the `dropMultipliers` config (1 = drop none, 0 = drop all).
12
+ *
13
+ * Schema-first: does not add new event properties or invent events. Operates on
14
+ * the funnelEvents array passed by the funnel-post hook.
15
+ */
16
+
17
+ import { binUsersByEventCount } from '../hook-helpers/cohort.js';
18
+ import { dropEventsWhere } from '../hook-helpers/mutate.js';
19
+
20
+ /**
21
+ * @param {Array<Object>} allUserEvents - Full per-user event history (read-only;
22
+ * used to count `cohortEvent`). When called inside `funnel-post`, derive this
23
+ * from `meta.profile` or pass the user's accumulated events from a closure.
24
+ * When `null`, falls back to counting cohortEvent inside `funnelEvents`.
25
+ * @param {Object} _profile
26
+ * @param {Array<Object>} funnelEvents - Funnel events produced by `makeFunnel`
27
+ * (mutated in place).
28
+ * @param {Object} opts
29
+ * @param {string} opts.cohortEvent
30
+ * @param {Record<string, [number, number]>} opts.bins
31
+ * @param {Record<string, number>} opts.dropMultipliers - Bin name → keep-rate (0..1)
32
+ * for the FINAL step event. 1 = always keep, 0 = always drop.
33
+ * @param {string} [opts.finalStep] - Event name of the final step. Defaults to the
34
+ * last event in `funnelEvents` (in time order).
35
+ * @returns {{ bin: string|null, droppedFinal: boolean }}
36
+ */
37
+ export function applyFunnelFrequencyBreakdown(allUserEvents, _profile, funnelEvents, opts) {
38
+ const { cohortEvent, bins, dropMultipliers, finalStep } = opts || {};
39
+ if (!funnelEvents || !cohortEvent || !bins || !dropMultipliers) {
40
+ return { bin: null, droppedFinal: false };
41
+ }
42
+ const sourceForBin = allUserEvents || funnelEvents;
43
+ const bin = binUsersByEventCount(sourceForBin, cohortEvent, bins);
44
+ if (!bin) return { bin: null, droppedFinal: false };
45
+ const keepRate = dropMultipliers[bin];
46
+ if (typeof keepRate !== 'number' || keepRate >= 1) return { bin, droppedFinal: false };
47
+
48
+ // Identify the final step. If finalStep is named, use it; otherwise pick the
49
+ // latest-in-time event in the funnel as the final step.
50
+ let stepName = finalStep;
51
+ if (!stepName) {
52
+ const sorted = funnelEvents.slice().sort((a, b) => Date.parse(a.time) - Date.parse(b.time));
53
+ stepName = sorted.length ? sorted[sorted.length - 1].event : null;
54
+ }
55
+ if (!stepName) return { bin, droppedFinal: false };
56
+
57
+ // Coin-flip drop using a deterministic-ish heuristic — this is called from a
58
+ // non-RNG context (funnel-post), so use Math.random would break determinism.
59
+ // Instead, use a hash on the funnel's first event's insert_id (deterministic
60
+ // per-call) modulo 1000 / 1000 vs. (1 - keepRate). For simplicity we use
61
+ // chance from utils when available.
62
+ const dropProb = 1 - keepRate;
63
+ const seed = funnelEvents[0] && (funnelEvents[0].insert_id || funnelEvents[0].time) || '';
64
+ const det = simpleHashFloat(String(seed));
65
+ if (det < dropProb) {
66
+ const before = funnelEvents.length;
67
+ dropEventsWhere(funnelEvents, e => e && e.event === stepName);
68
+ return { bin, droppedFinal: funnelEvents.length < before };
69
+ }
70
+ return { bin, droppedFinal: false };
71
+ }
72
+
73
+ import { simpleHashFloat } from '../hook-helpers/_internal.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @ak--47/dungeon-master/hook-patterns — Phase 4 pattern barrel.
3
+ *
4
+ * Patterns are higher-level recipes built on Phase 3 atoms. Each one engineers
5
+ * the kind of distribution / table shape Mixpanel surfaces in a specific report.
6
+ * Pair with `verifyDungeon` + `emulateBreakdown` from `../verify` to assert the
7
+ * pattern is producing what you expect.
8
+ */
9
+
10
+ export { applyFrequencyByFrequency } from './frequency-by-frequency.js';
11
+ export { applyFunnelFrequencyBreakdown } from './funnel-frequency-breakdown.js';
12
+ export { applyAggregateByBin } from './aggregate-per-user-by-bin.js';
13
+ export { applyTTCBySegment } from './time-to-convert-by-segment.js';
14
+ export { applyAttributedBySource } from './attributed-by-source.js';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pattern: Time to Convert, broken down by user segment.
3
+ *
4
+ * Inside a `funnel-post` hook, scale the funnel's time-to-convert by a factor
5
+ * keyed off of a user-profile property value (e.g., trial users convert 3x
6
+ * slower than enterprise). Lets Mixpanel's TTC funnel report — broken down by
7
+ * a profile property — show a deliberate spread.
8
+ *
9
+ * Mechanism: read `profile[segmentKey]`, look up the multiplier in `factors`,
10
+ * and `scaleFunnelTTC(funnelEvents, factor)`. The first event's time is the
11
+ * anchor (unchanged); subsequent steps' offsets from it are scaled.
12
+ *
13
+ * Caveat: Mixpanel's "Time to Convert" funnel report uses the time between the
14
+ * FIRST event of step A and the FIRST event of step B per user, not the actual
15
+ * gap inside any one funnel run. The scaled funnel run will reflect in TTC only
16
+ * when this funnel is the user's first occurrence of those steps — which it is
17
+ * for an `isFirstFunnel`. For usage funnels, document this caveat to authors.
18
+ */
19
+
20
+ import { scaleFunnelTTC } from '../hook-helpers/timing.js';
21
+
22
+ /**
23
+ * @param {Array<Object>} funnelEvents - Mutated in place.
24
+ * @param {Object} profile - The user's profile (must contain `segmentKey`).
25
+ * @param {Object} opts
26
+ * @param {string} opts.segmentKey - Profile property name to look up.
27
+ * @param {Record<string, number>} opts.factors - Profile-value → TTC factor.
28
+ * @returns {{ segmentValue: any, factor: number, shifted: number }}
29
+ */
30
+ export function applyTTCBySegment(funnelEvents, profile, { segmentKey, factors }) {
31
+ if (!funnelEvents || !funnelEvents.length || !profile || !segmentKey || !factors) {
32
+ return { segmentValue: null, factor: 1, shifted: 0 };
33
+ }
34
+ const segmentValue = profile[segmentKey];
35
+ const factor = factors[segmentValue];
36
+ if (typeof factor !== 'number' || factor === 1) {
37
+ return { segmentValue, factor: 1, shifted: 0 };
38
+ }
39
+ const shifted = scaleFunnelTTC(funnelEvents, factor);
40
+ return { segmentValue, factor, shifted };
41
+ }