@ak--47/dungeon-master 1.5.4 → 1.6.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 (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +183 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +2 -1
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. package/dungeons/vertical/travel.js +0 -930
@@ -0,0 +1,249 @@
1
+ -- ============================================================
2
+ -- gaming.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in gaming.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/gaming/gaming.js --data-prefix verify-gaming
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js verify-gaming
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/gaming/gaming.sql"
11
+ -- ============================================================
12
+
13
+ -- ── identity-resolution prelude ─────────────────────────────
14
+ -- avgDevicePerUser: 2 — resolve device_id → canonical distinct_id via the
15
+ -- USERS shards so uniques are identity-correct even for device-only rows
16
+ -- (pre-auth steps of the first funnel carry device_id only).
17
+ CREATE OR REPLACE VIEW users AS
18
+ SELECT * FROM read_json_auto('data/verify-gaming-USERS*.json', sample_size=-1, union_by_name=true);
19
+
20
+ CREATE OR REPLACE VIEW device_map AS
21
+ -- profiles store the device pool under the legacy "anonymousIds" key
22
+ -- (buildIdentityMap in lib/verify/identity.js reads the same field)
23
+ SELECT unnest("anonymousIds") AS device_id, distinct_id FROM users;
24
+
25
+ CREATE OR REPLACE VIEW ev AS
26
+ -- ::VARCHAR casts — user_id sniffs as UUID, device_id as VARCHAR; DuckDB
27
+ -- refuses to coalesce mixed types
28
+ SELECT coalesce(m.distinct_id::VARCHAR, e.user_id::VARCHAR, e.device_id::VARCHAR) AS uid,
29
+ e.time::TIMESTAMP AS t,
30
+ e.*
31
+ FROM read_json_auto('data/verify-gaming-EVENTS*.json', sample_size=-1, union_by_name=true) e
32
+ LEFT JOIN device_map m ON e.device_id = m.device_id;
33
+
34
+
35
+ -- ── H1-compass-heavy-rewards ────────────────────────────────
36
+ -- heavy compass users (2+ "use item" w/ Ancient Compass, ~48%) earn ~1.5x
37
+ -- reward_gold and reward_xp on quest turned in (measured 1.56x / 1.58x —
38
+ -- bonus-quest clones are built at the boosted rate)
39
+ WITH cnt AS (
40
+ SELECT u.distinct_id AS uid,
41
+ count(e.uid) FILTER (WHERE e.event = 'use item' AND e.item_type = 'Ancient Compass') AS n
42
+ FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id GROUP BY 1
43
+ )
44
+ SELECT CASE WHEN c.n >= 2 THEN 'heavy' ELSE 'light' END AS cohort,
45
+ count(DISTINCT c.uid) AS users,
46
+ round(avg(e.reward_gold), 1) AS avg_gold,
47
+ round(avg(e.reward_xp), 1) AS avg_xp
48
+ FROM ev e JOIN cnt c ON e.uid = c.uid
49
+ WHERE e.event = 'quest turned in'
50
+ GROUP BY 1 ORDER BY 1;
51
+
52
+
53
+ -- ── H2-cursed-week ──────────────────────────────────────────
54
+ -- injected Curse deaths cluster in days 40-47 of each user's own timeline.
55
+ -- Measured on 48d+ lifetime users: per-day density in/out ≈ 36x; raw in/out
56
+ -- count ≈ 2.7 (out-of-window span is ~10x longer)
57
+ WITH firsts AS (SELECT uid, min(t) AS t0, max(t) AS tN FROM ev GROUP BY 1),
58
+ longlife AS (SELECT uid, t0, tN FROM firsts WHERE tN >= t0 + INTERVAL 48 DAY),
59
+ curse AS (
60
+ SELECT e.uid, epoch(e.t - l.t0) / 86400.0 AS dol
61
+ FROM ev e JOIN longlife l ON e.uid = l.uid
62
+ WHERE e.event = 'player death' AND e.cause_of_death = 'Curse'
63
+ )
64
+ SELECT
65
+ count(*) FILTER (WHERE dol BETWEEN 40 AND 47) AS in_window,
66
+ count(*) FILTER (WHERE dol < 40 OR dol > 47) AS out_window,
67
+ (SELECT count(*) FROM longlife) AS longlife_users,
68
+ round((count(*) FILTER (WHERE dol BETWEEN 40 AND 47) / 8.0)
69
+ / nullif(count(*) FILTER (WHERE dol < 40 OR dol > 47)
70
+ / ((SELECT avg(epoch(tN - t0)) / 86400.0 FROM longlife) - 8), 0), 1) AS density_ratio
71
+ FROM curse;
72
+
73
+
74
+ -- ── H3-guild-rescue ─────────────────────────────────────────
75
+ -- among users with 3+ week-1 deaths: early guild joiners (first 3 days) are
76
+ -- exempt from the spiral → ~2.5x post-week-1 volume vs spiraled peers
77
+ -- (knob ceiling 1/0.3 ≈ 3.3x; both cohorts select front-loaded players)
78
+ WITH firsts AS (SELECT uid, min(t) AS t0 FROM ev GROUP BY 1),
79
+ flags AS (
80
+ SELECT f.uid,
81
+ count(e.uid) FILTER (WHERE e.event = 'player death' AND e.t < f.t0 + INTERVAL 7 DAY) AS early_deaths,
82
+ count(e.uid) FILTER (WHERE e.event = 'guild joined' AND e.t < f.t0 + INTERVAL 3 DAY) AS early_guild,
83
+ count(e.uid) FILTER (WHERE e.t >= f.t0 + INTERVAL 7 DAY) AS post_events
84
+ FROM firsts f JOIN ev e ON e.uid = f.uid GROUP BY 1
85
+ )
86
+ SELECT CASE WHEN early_guild > 0 THEN 'guild_saved' ELSE 'spiral' END AS cohort,
87
+ count(*) AS users, round(avg(post_events), 1) AS avg_post
88
+ FROM flags WHERE early_deaths >= 3 GROUP BY 1 ORDER BY 1;
89
+
90
+
91
+ -- ── H4-death-spiral ─────────────────────────────────────────
92
+ -- 3+ week-1 deaths and no early guild → 70% of post-week-1 events dropped.
93
+ -- spiral post/pre event ratio ~0.11 vs healthy; spiral cohort ~11% of the
94
+ -- guild-free population
95
+ WITH firsts AS (SELECT uid, min(t) AS t0 FROM ev GROUP BY 1),
96
+ flags AS (
97
+ SELECT f.uid,
98
+ count(e.uid) FILTER (WHERE e.event = 'player death' AND e.t < f.t0 + INTERVAL 7 DAY) AS early_deaths,
99
+ count(e.uid) FILTER (WHERE e.event = 'guild joined' AND e.t < f.t0 + INTERVAL 3 DAY) AS early_guild,
100
+ count(e.uid) FILTER (WHERE e.t >= f.t0 + INTERVAL 7 DAY) AS post_events,
101
+ count(e.uid) FILTER (WHERE e.t < f.t0 + INTERVAL 7 DAY) AS pre_events
102
+ FROM firsts f JOIN ev e ON e.uid = f.uid GROUP BY 1
103
+ )
104
+ SELECT CASE WHEN early_guild = 0 AND early_deaths >= 3 THEN 'spiral'
105
+ WHEN early_guild > 0 THEN 'guild' ELSE 'other' END AS cohort,
106
+ count(*) AS users,
107
+ round(avg(post_events)::DOUBLE / nullif(avg(pre_events), 0), 3) AS post_pre
108
+ FROM flags GROUP BY 1 ORDER BY 1;
109
+
110
+
111
+ -- ── H5-lucky-charm ──────────────────────────────────────────
112
+ -- Lucky Charm Pack buyers (~9% of spenders) pay 2.5x on the pack itself and
113
+ -- get 35% bonus-purchase clones → avg spend ~3.4x non-buyers. Scoped to
114
+ -- NON-whales so H10's 1.8x multiplier doesn't contaminate the ratio.
115
+ WITH lucky AS (SELECT DISTINCT uid FROM ev WHERE event = 'real money purchase' AND product = 'Lucky Charm Pack')
116
+ SELECT
117
+ round(avg(price_usd) FILTER (WHERE uid IN (SELECT uid FROM lucky)), 2) AS lucky_avg,
118
+ round(avg(price_usd) FILTER (WHERE uid NOT IN (SELECT uid FROM lucky)), 2) AS nonlucky_avg,
119
+ count(DISTINCT uid) FILTER (WHERE uid IN (SELECT uid FROM lucky)) AS lucky_users
120
+ FROM ev WHERE event = 'real money purchase' AND price_usd IS NOT NULL
121
+ AND substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f');
122
+
123
+
124
+ -- ── H6-strategic-explorers ──────────────────────────────────
125
+ -- strategic explorers (6+ inspect AND 6+ search for clues, ~42% of users):
126
+ -- 85% dungeon completion vs ~54% baseline, 2x treasure (measured 1.7x blend)
127
+ WITH cnt AS (
128
+ SELECT u.distinct_id AS uid,
129
+ count(e.uid) FILTER (WHERE e.event = 'inspect') AS ins,
130
+ count(e.uid) FILTER (WHERE e.event = 'search for clues') AS sea
131
+ FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id GROUP BY 1
132
+ ),
133
+ coh AS (SELECT uid, (ins >= 6 AND sea >= 6) AS strategic FROM cnt)
134
+ SELECT c.strategic, count(DISTINCT c.uid) AS users,
135
+ round(avg(e.treasure_value) FILTER (WHERE e.event = 'find treasure'), 1) AS avg_treasure,
136
+ round((count(*) FILTER (WHERE e.event = 'exit dungeon' AND e.completion_status = 'completed'))::DOUBLE
137
+ / nullif(count(*) FILTER (WHERE e.event = 'exit dungeon'), 0), 3) AS completion
138
+ FROM coh c JOIN ev e ON e.uid = c.uid GROUP BY 1 ORDER BY 1;
139
+
140
+
141
+ -- ── H7-shadowmourne ─────────────────────────────────────────
142
+ -- Shadowmourne Legendary drops only after dataset day 45 (2% roll on find
143
+ -- treasure); zero pre-release sightings; owners win ~96% of combats vs ~55%
144
+ WITH owners AS (SELECT DISTINCT uid FROM ev WHERE event = 'find treasure' AND treasure_type = 'Shadowmourne Legendary'),
145
+ combat AS (
146
+ SELECT uid, count(*) FILTER (WHERE outcome = 'Victory')::DOUBLE / count(*) AS win_rate
147
+ FROM ev WHERE event = 'combat completed' GROUP BY 1
148
+ )
149
+ SELECT
150
+ (SELECT count(*) FROM ev WHERE event = 'find treasure' AND treasure_type = 'Shadowmourne Legendary'
151
+ AND t < (SELECT min(t) FROM ev) + INTERVAL 45 DAY) AS pre_release_drops,
152
+ (SELECT count(*) FROM owners) AS owners,
153
+ (SELECT round(avg(win_rate), 3) FROM combat WHERE uid IN (SELECT uid FROM owners)) AS owner_win,
154
+ (SELECT round(avg(win_rate), 3) FROM combat WHERE uid NOT IN (SELECT uid FROM owners)) AS rest_win;
155
+
156
+
157
+ -- ── H8-subscriber-tiers ─────────────────────────────────────
158
+ -- Elite: 1.8x quest gold, completion flip → ~0.90; Premium: 1.4x gold,
159
+ -- completion → ~0.82; Free baseline ~0.68 (completion = f + (1-f)·flip)
160
+ SELECT subscription_tier, count(DISTINCT uid) AS users,
161
+ round(avg(reward_gold) FILTER (WHERE event = 'quest turned in'), 1) AS avg_gold,
162
+ round((count(*) FILTER (WHERE event = 'exit dungeon' AND completion_status = 'completed'))::DOUBLE
163
+ / nullif(count(*) FILTER (WHERE event = 'exit dungeon'), 0), 3) AS completion
164
+ FROM ev GROUP BY 1 ORDER BY 1;
165
+
166
+
167
+ -- ── H9-level-gold-scaling ───────────────────────────────────
168
+ -- reward_gold scales with profile level: mult = 1 + 0.15·(level-1).
169
+ -- formula check: hi/lo gold ratio should equal
170
+ -- (1 + 0.15·(mean_hi - 1)) / (1 + 0.15·(mean_lo - 1)) — measured 1.95 vs 1.94
171
+ WITH bucketed AS (
172
+ SELECT CASE WHEN u.level <= 5 THEN 'lo_1_5' WHEN u.level >= 16 THEN 'hi_16_20' ELSE 'mid' END AS bucket,
173
+ u.level, e.reward_gold
174
+ FROM users u JOIN ev e ON e.uid = u.distinct_id AND e.event = 'quest turned in'
175
+ )
176
+ SELECT bucket, count(*) AS quests, round(avg(level), 2) AS mean_level,
177
+ round(avg(reward_gold), 1) AS avg_gold,
178
+ round((1 + 0.15 * (avg(level) - 1)), 3) AS predicted_mult
179
+ FROM bucketed GROUP BY 1 ORDER BY 1;
180
+
181
+
182
+ -- ── H10-whale-purchases ─────────────────────────────────────
183
+ -- whales = distinct_id first hex char in ('0','3','6','9','c','f') → 37.5%
184
+ -- of ids nominally, ~63% of spend rows; 1.8x price_usd. Scoped to NON-lucky
185
+ -- buyers so H5's multiplier doesn't contaminate the ratio.
186
+ WITH lucky AS (SELECT DISTINCT uid FROM ev WHERE event = 'real money purchase' AND product = 'Lucky Charm Pack')
187
+ SELECT
188
+ round(avg(price_usd) FILTER (WHERE substr(uid, 1, 1) IN ('0','3','6','9','c','f')), 2) AS whale_avg,
189
+ round(avg(price_usd) FILTER (WHERE substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f')), 2) AS rest_avg,
190
+ count(DISTINCT uid) FILTER (WHERE substr(uid, 1, 1) IN ('0','3','6','9','c','f')) AS whale_users,
191
+ count(DISTINCT uid) FILTER (WHERE substr(uid, 1, 1) NOT IN ('0','3','6','9','c','f')) AS rest_users
192
+ FROM ev WHERE event = 'real money purchase' AND price_usd IS NOT NULL
193
+ AND uid NOT IN (SELECT uid FROM lucky);
194
+
195
+
196
+ -- ── H11-alignment-archetype ─────────────────────────────────
197
+ -- deterministic profile mapping: Good alignments → hero, Evil → villain,
198
+ -- rest → neutral. Counts must match EXACTLY; shares ≈ 26/25/49
199
+ SELECT
200
+ count(*) FILTER (WHERE alignment IN ('Lawful Good', 'Neutral Good')) AS good_aligns,
201
+ count(*) FILTER (WHERE archetype = 'hero') AS heroes,
202
+ count(*) FILTER (WHERE alignment IN ('Chaotic Evil', 'Neutral Evil')) AS evil_aligns,
203
+ count(*) FILTER (WHERE archetype = 'villain') AS villains,
204
+ count(*) FILTER (WHERE archetype = 'neutral') AS neutrals,
205
+ count(*) AS total
206
+ FROM users;
207
+
208
+
209
+ -- ── H12-combat-ttc-by-tier ──────────────────────────────────
210
+ -- funnel-post compresses combat initiated → combat completed → use item by
211
+ -- tier (Elite 0.30x, Premium 0.70x, Free 1.40x). The story uses the funnel
212
+ -- emulator (median TTC, 6h window); this cross-event MIN→MIN approximation
213
+ -- shows the same ordering for eyeball purposes.
214
+ WITH ci AS (SELECT uid, min(t) AS t0 FROM ev WHERE event = 'combat initiated' GROUP BY 1),
215
+ ui AS (
216
+ SELECT e.uid, min(e.t) AS t1
217
+ FROM ev e JOIN ci ON e.uid = ci.uid AND e.t >= ci.t0
218
+ WHERE e.event = 'use item' GROUP BY 1
219
+ )
220
+ SELECT u.subscription_tier, count(*) AS converters,
221
+ round(median(epoch(t1 - t0)) / 60, 1) AS median_ttc_min
222
+ FROM ci JOIN ui USING (uid) JOIN users u ON u.distinct_id = ci.uid
223
+ GROUP BY 1 ORDER BY median_ttc_min;
224
+
225
+
226
+ -- ── H13-prep-magic-number ───────────────────────────────────
227
+ -- prep actions (inspect + search for clues) between first quest accepted and
228
+ -- first fight boss: sweet band 3-6 → 1.3x treasure (measured 1.24x scoped to
229
+ -- non-strategic users); over-prepared 7+ → boss win flipped to ~0.73x sweet
230
+ WITH anchors AS (
231
+ SELECT uid,
232
+ min(t) FILTER (WHERE event = 'quest accepted') AS qa,
233
+ min(t) FILTER (WHERE event = 'fight boss') AS fb
234
+ FROM ev GROUP BY 1
235
+ ),
236
+ prep AS (
237
+ SELECT a.uid, count(e.uid) AS n
238
+ FROM anchors a LEFT JOIN ev e ON e.uid = a.uid
239
+ AND e.event IN ('inspect', 'search for clues') AND e.t > a.qa AND e.t < a.fb
240
+ WHERE a.qa IS NOT NULL AND a.fb IS NOT NULL AND a.fb > a.qa
241
+ GROUP BY 1
242
+ ),
243
+ bands AS (SELECT uid, CASE WHEN n BETWEEN 3 AND 6 THEN 'sweet' WHEN n < 3 THEN 'low' ELSE 'over' END AS band FROM prep)
244
+ SELECT b.band, count(DISTINCT b.uid) AS users,
245
+ round(avg(e.treasure_value) FILTER (WHERE e.event = 'find treasure'), 1) AS avg_treasure,
246
+ round((count(*) FILTER (WHERE e.event = 'fight boss' AND e.victory = true))::DOUBLE
247
+ / nullif(count(*) FILTER (WHERE e.event = 'fight boss'), 0), 3) AS boss_win
248
+ FROM bands b JOIN ev e ON e.uid = b.uid
249
+ GROUP BY 1 ORDER BY 1;
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gaming.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./gaming.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/gaming/gaming.js --data-prefix verify-gaming
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js verify-gaming
12
+ * Run:
13
+ * node dungeons/vertical/gaming/gaming.verify.mjs [prefix] # default verify-gaming
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import readline from 'node:readline';
18
+ import { execFile } from 'node:child_process';
19
+ import { promisify } from 'node:util';
20
+ import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
21
+ import config, { stories } from './gaming.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-gaming';
24
+ const prefixPath = path.join('data', PREFIX);
25
+
26
+ async function loadShards(suffix) {
27
+ // streaming load: full-fidelity EVENTS shards exceed the readFileSync cap
28
+ const dir = path.dirname(prefixPath), base = path.basename(prefixPath);
29
+ const out = [];
30
+ if (!fs.existsSync(dir)) return out;
31
+ for (const f of fs.readdirSync(dir).filter(f => f.startsWith(`${base}-${suffix}`) && f.endsWith('.json')).sort()) {
32
+ const rl = readline.createInterface({ input: fs.createReadStream(path.join(dir, f)), crlfDelay: Infinity });
33
+ for await (const line of rl) {
34
+ if (line.trim()) out.push(JSON.parse(line));
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ const events = await loadShards('EVENTS');
41
+ const profiles = await loadShards('USERS');
42
+ if (!events.length) {
43
+ console.error(`no shards at ${prefixPath}-EVENTS*.json — run: node scripts/verify-runner.mjs dungeons/vertical/gaming/gaming.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`gaming — events=${events.length} users=${profiles.length} (${prefixPath})`);
47
+
48
+ const execFileP = promisify(execFile);
49
+ const runSql = async (sql) => {
50
+ const { stdout } = await execFileP('duckdb', ['-json', '-c', sql.replaceAll('{{PREFIX}}', prefixPath)], { maxBuffer: 64 * 1024 * 1024 });
51
+ return stdout.trim() ? JSON.parse(stdout) : [];
52
+ };
53
+
54
+ // funnels passed raw (unvalidated) — every timeToConvert story carries an
55
+ // explicit conversionWindowMs, so no funnel-default threading is needed
56
+ const results = await evaluateStories(stories, events, {
57
+ profiles,
58
+ funnels: config.funnels,
59
+ identityMap: buildIdentityMap(profiles),
60
+ runSql,
61
+ });
62
+
63
+ let worst = 'NAILED';
64
+ for (const r of results) {
65
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
66
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
67
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
68
+ }
69
+ console.log(`\nworst verdict: ${worst}`);
70
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);