@ak--47/dungeon-master 1.5.3 → 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 +96 -0
  4. package/.claude/skills/create-project/provision.mjs +336 -0
  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 +211 -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 +3 -2
  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,218 @@
1
+ -- ============================================================
2
+ -- ecommerce.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in ecommerce.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/ecommerce/ecommerce.js --data-prefix verify-ecommerce
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/ecommerce/ecommerce.js verify-ecommerce
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/ecommerce/ecommerce.sql"
11
+ -- ============================================================
12
+
13
+ -- ── identity-resolution prelude ─────────────────────────────
14
+ -- avgDevicePerUser: 2 + sign up isAuthEvent — born-in users' pre-auth
15
+ -- Signup Flow steps carry device_id ONLY; resolve through the profile's
16
+ -- device pool so per-user aggregations are identity-correct.
17
+ CREATE OR REPLACE VIEW users AS
18
+ SELECT * FROM read_json_auto('data/verify-ecommerce-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-ecommerce-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
+ CREATE OR REPLACE VIEW scd AS
35
+ SELECT * FROM read_json_auto('data/verify-ecommerce-loyalty_tier-SCD*.json', sample_size=-1, union_by_name=true);
36
+
37
+
38
+ -- ── H1-signup-fix ───────────────────────────────────────────
39
+ -- signup_flow flips v1 → v2 at datasetEnd - 7d; 50% of pre-fix signups
40
+ -- dropped. Purity is exact (0 v2 pre, 0 v1 post); daily-rate jump is a
41
+ -- 2x-drop × acquisition-ramp composite (~7x measured).
42
+ SELECT
43
+ count(*) FILTER (WHERE signup_flow = 'v2' AND t < TIMESTAMP '2026-04-24 23:59:59') AS v2_pre,
44
+ count(*) FILTER (WHERE signup_flow = 'v1' AND t > TIMESTAMP '2026-04-24 23:59:59') AS v1_post,
45
+ round(count(*) FILTER (WHERE t >= TIMESTAMP '2026-04-24 23:59:59') / 7.0, 2) AS post_daily,
46
+ round(count(*) FILTER (WHERE t < TIMESTAMP '2026-04-24 23:59:59') / 114.0, 2) AS pre_daily,
47
+ round((count(*) FILTER (WHERE t >= TIMESTAMP '2026-04-24 23:59:59') / 7.0)
48
+ / nullif(count(*) FILTER (WHERE t < TIMESTAMP '2026-04-24 23:59:59') / 114.0, 0), 2) AS daily_ratio
49
+ FROM ev WHERE event = 'sign up';
50
+
51
+
52
+ -- ── H2-watch-inflection ─────────────────────────────────────
53
+ -- watchTimeSec ×(1-f) before datasetEnd - 30d, ×(1+f) after, f ~ U[0.25, 0.79]
54
+ -- → post/pre avg = E[1+f]/E[1-f] = 1.52/0.48 ≈ 3.17
55
+ SELECT
56
+ round(avg(watchTimeSec) FILTER (WHERE t < TIMESTAMP '2026-04-01 23:59:59'), 1) AS pre_avg,
57
+ round(avg(watchTimeSec) FILTER (WHERE t >= TIMESTAMP '2026-04-01 23:59:59'), 1) AS post_avg,
58
+ round(avg(watchTimeSec) FILTER (WHERE t >= TIMESTAMP '2026-04-01 23:59:59')
59
+ / nullif(avg(watchTimeSec) FILTER (WHERE t < TIMESTAMP '2026-04-01 23:59:59'), 0), 3) AS ratio
60
+ FROM ev WHERE event = 'watch video';
61
+
62
+
63
+ -- ── H3-toys-shoes-basket ────────────────────────────────────
64
+ -- toys carts get a shoes item injected when the donor cart has one (~49%)
65
+ -- and vice versa; neither-carts get amounts ×U[0.75, 0.9] (mean 0.825).
66
+ -- P(shoes|toys) ≈ 0.64-0.71 vs P(shoes|no toys) ≈ 0.11 → ~6x lift.
67
+ WITH x AS (SELECT insert_id, unnest(cart) AS item FROM ev WHERE event = 'checkout' AND cart IS NOT NULL),
68
+ flags AS (SELECT insert_id, bool_or(item.category = 'toys') AS has_toys,
69
+ bool_or(item.category = 'shoes') AS has_shoes, avg(item.amount) AS avg_amt FROM x GROUP BY 1)
70
+ SELECT count(*) AS carts,
71
+ round(count(*) FILTER (WHERE has_toys AND has_shoes)::DOUBLE / nullif(count(*) FILTER (WHERE has_toys), 0), 3) AS p_shoes_given_toys,
72
+ round(count(*) FILTER (WHERE has_shoes AND NOT has_toys)::DOUBLE / nullif(count(*) FILTER (WHERE NOT has_toys), 0), 3) AS p_shoes_given_no_toys,
73
+ round(avg(avg_amt) FILTER (WHERE NOT has_toys AND NOT has_shoes)
74
+ / nullif(avg(avg_amt) FILTER (WHERE has_toys OR has_shoes), 0), 3) AS neither_either_ratio
75
+ FROM flags;
76
+
77
+
78
+ -- ── H4-quality-watchtime ────────────────────────────────────
79
+ -- watchTimeSec × quality factor (240p 0.7 … 2160p 1.5) → strict monotone,
80
+ -- 2160p/240p ≈ 2.14 (H2's temporal factor is quality-blind and cancels)
81
+ SELECT quality, count(*) AS n, round(avg(watchTimeSec), 1) AS avg_watch
82
+ FROM ev WHERE event = 'watch video' GROUP BY 1 ORDER BY avg_watch;
83
+
84
+
85
+ -- ── H5-item-flattening ──────────────────────────────────────
86
+ -- view item / add to cart / save item: item[0] spread top-level, nested
87
+ -- item deleted (no `item` column survives in shards — DESCRIBE proves it);
88
+ -- checkout's cart stays nested 100%. Flattened slug is always the
89
+ -- "<descriptor>-<suffix>" compound, never the schema default "item".
90
+ SELECT count(*) FILTER (WHERE column_name = 'item') AS item_cols,
91
+ count(*) FILTER (WHERE column_name = 'cart') AS cart_cols
92
+ FROM (DESCRIBE SELECT * FROM read_json_auto('data/verify-ecommerce-EVENTS*.json', sample_size=-1, union_by_name=true));
93
+
94
+ SELECT event, count(*) AS n,
95
+ count(*) FILTER (WHERE slug LIKE '%-%') AS compound_slug,
96
+ count(*) FILTER (WHERE category IS NOT NULL) AS with_category,
97
+ count(*) FILTER (WHERE cart IS NOT NULL) AS with_cart
98
+ FROM ev WHERE event IN ('view item', 'add to cart', 'save item', 'checkout')
99
+ GROUP BY 1 ORDER BY 1;
100
+
101
+
102
+ -- ── H6-view-magic-number ────────────────────────────────────
103
+ -- 3-8 view items → cart amounts ×1.25 + 45% add-to-cart clones; 9+ views →
104
+ -- 30% of checkouts dropped. View count is funnel-dominated (over bin ≈ 89%
105
+ -- of users) so the signals are per-item amount (clean 1.25), carts-per-view
106
+ -- (~1.37 clone composite), checkouts-per-cart (~0.54 drop composite).
107
+ WITH vc AS (
108
+ SELECT u.distinct_id::VARCHAR AS duid,
109
+ count(e.uid) FILTER (WHERE e.event = 'view item') AS views,
110
+ count(e.uid) FILTER (WHERE e.event = 'add to cart') AS carts,
111
+ count(e.uid) FILTER (WHERE e.event = 'checkout') AS cks
112
+ FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id::VARCHAR GROUP BY 1),
113
+ bins AS (SELECT duid, views, carts, cks,
114
+ CASE WHEN views BETWEEN 3 AND 8 THEN 'sweet' WHEN views < 3 THEN 'low' ELSE 'over' END AS bin FROM vc),
115
+ items AS (SELECT e.uid, unnest(e.cart) AS item FROM ev e WHERE e.event = 'checkout' AND e.cart IS NOT NULL)
116
+ SELECT b.bin, count(DISTINCT b.duid) AS users,
117
+ round(avg(i.item.amount), 2) AS avg_cart_item_amt
118
+ FROM bins b LEFT JOIN items i ON i.uid = b.duid GROUP BY 1 ORDER BY 1;
119
+
120
+ WITH vc AS (
121
+ SELECT u.distinct_id::VARCHAR AS duid,
122
+ count(e.uid) FILTER (WHERE e.event = 'view item') AS views,
123
+ count(e.uid) FILTER (WHERE e.event = 'add to cart') AS carts,
124
+ count(e.uid) FILTER (WHERE e.event = 'checkout') AS cks
125
+ FROM users u LEFT JOIN ev e ON e.uid = u.distinct_id::VARCHAR GROUP BY 1)
126
+ SELECT CASE WHEN views BETWEEN 3 AND 8 THEN 'sweet' WHEN views < 3 THEN 'low' ELSE 'over' END AS bin,
127
+ count(*) AS users,
128
+ round(sum(carts)::DOUBLE / nullif(sum(views), 0), 4) AS carts_per_view,
129
+ round(sum(cks)::DOUBLE / nullif(sum(carts), 0), 4) AS ck_per_cart
130
+ FROM vc GROUP BY 1 ORDER BY 1;
131
+
132
+
133
+ -- ── H7-loyalty-signup-ttc ───────────────────────────────────
134
+ -- Signup Flow TTC scaled by latest SCD loyalty_tier: gold/platinum ×0.67,
135
+ -- bronze ×1.33 → bronze/(gold+plat) pure-scale ratio 1.99, measured
136
+ -- ~1.85-2.04 across seeds (organic events inside the 2d lookback dilute;
137
+ -- re-windowing on scaled times censors tails)
138
+ WITH latest AS (
139
+ SELECT distinct_id, loyalty_tier FROM (
140
+ SELECT distinct_id, loyalty_tier, row_number() OVER (PARTITION BY distinct_id ORDER BY time DESC) AS rn
141
+ FROM scd) WHERE rn = 1),
142
+ su AS (SELECT uid, min(t) AS st FROM ev WHERE event = 'sign up' GROUP BY 1),
143
+ steps AS (
144
+ SELECT s.uid, s.st, min(e.t) AS first_step
145
+ FROM su s JOIN ev e ON e.uid = s.uid
146
+ WHERE e.event IN ('page view', 'view item', 'save item')
147
+ AND e.t >= s.st - INTERVAL 2 DAY AND e.t <= s.st GROUP BY 1, 2)
148
+ SELECT CASE WHEN l.loyalty_tier IN ('gold', 'platinum') THEN 'fast'
149
+ WHEN l.loyalty_tier = 'bronze' THEN 'slow' ELSE 'silver' END AS grp,
150
+ count(*) AS users, round(median(epoch(st - first_step)) / 60, 1) AS med_ttc_min
151
+ FROM steps s JOIN latest l ON l.distinct_id = s.uid GROUP BY 1 ORDER BY med_ttc_min;
152
+
153
+
154
+ -- ── H8-checkout-experiment ──────────────────────────────────
155
+ -- engine experiment on eCommerce Purchase (last 30 days): deterministic
156
+ -- per-user hash → equal thirds; theme-composed per-attempt conversion
157
+ -- Control ≈ 16.2%, Express ≈ 20.2%, Social ≈ 17.8%; Social ttc ×0.9.
158
+ -- STRICT pairing: checkout within 75min AND >= 5 view/add steps between
159
+ -- $experiment_started and the checkout. Naive time-window pairing is
160
+ -- polluted by same-session organic weight-2 checkouts (~48K organic vs
161
+ -- ~2K funnel checkouts), which compresses both ratios toward 1 (measured
162
+ -- 1.06 conv ratio / 34min mean TTC naive vs ~1.25 / ~60min strict).
163
+ WITH att AS (SELECT user_id::VARCHAR AS auid, time::TIMESTAMP AS att_t, "Variant name" AS variant
164
+ FROM read_json_auto('data/verify-ecommerce-EVENTS*.json', sample_size=-1, union_by_name=true)
165
+ WHERE event = '$experiment_started'),
166
+ ck AS (SELECT user_id::VARCHAR AS cuid, time::TIMESTAMP AS ct
167
+ FROM read_json_auto('data/verify-ecommerce-EVENTS*.json', sample_size=-1, union_by_name=true)
168
+ WHERE event = 'checkout'),
169
+ naive AS (
170
+ SELECT a.auid, a.variant, a.att_t, min(c.ct) AS ct
171
+ FROM att a LEFT JOIN ck c ON c.cuid = a.auid AND c.ct > a.att_t AND c.ct <= a.att_t + INTERVAL 75 MINUTE
172
+ GROUP BY 1, 2, 3),
173
+ mids AS (
174
+ SELECT n.auid, n.att_t, count(*) AS steps
175
+ FROM naive n JOIN read_json_auto('data/verify-ecommerce-EVENTS*.json', sample_size=-1, union_by_name=true) s
176
+ ON s.user_id::VARCHAR = n.auid AND s.event IN ('view item', 'add to cart')
177
+ AND s.time::TIMESTAMP > n.att_t AND s.time::TIMESTAMP < n.ct
178
+ WHERE n.ct IS NOT NULL GROUP BY 1, 2),
179
+ paired AS (
180
+ SELECT n.auid, n.variant, n.att_t,
181
+ CASE WHEN m.steps >= 5 THEN n.ct ELSE NULL END AS conv_t
182
+ FROM naive n LEFT JOIN mids m ON m.auid = n.auid AND m.att_t = n.att_t)
183
+ SELECT variant, count(DISTINCT auid) AS users, count(*) AS attempts, count(conv_t) AS conversions,
184
+ round(count(conv_t)::DOUBLE / count(*), 4) AS conv_rate,
185
+ round(avg(epoch(conv_t - att_t)) FILTER (WHERE conv_t IS NOT NULL) / 60, 1) AS mean_ttc_min
186
+ FROM paired GROUP BY 1 ORDER BY 1;
187
+
188
+
189
+ -- ── H9-dark-theme-power ─────────────────────────────────────
190
+ -- funnel-pre scales purchase conversionRate: dark 20%, light 13%, custom 15%
191
+ -- → per-user checkouts dark/light ≈ 1.48 (organic weight-2 checkouts dilute)
192
+ WITH uc AS (SELECT theme, count(*) AS users FROM users GROUP BY 1),
193
+ ec AS (SELECT theme, count(*) AS cks
194
+ FROM read_json_auto('data/verify-ecommerce-EVENTS*.json', sample_size=-1, union_by_name=true)
195
+ WHERE event = 'checkout' GROUP BY 1)
196
+ SELECT uc.theme, uc.users, ec.cks AS checkouts,
197
+ round(ec.cks::DOUBLE / uc.users, 3) AS checkouts_per_user
198
+ FROM uc JOIN ec ON uc.theme = ec.theme ORDER BY checkouts_per_user DESC;
199
+
200
+
201
+ -- ── H10-save-retention ──────────────────────────────────────
202
+ -- born users with <2 save items in first 10d lose 70% of post-day-25
203
+ -- events. Every signer has one funnel-guaranteed save (Signup Flow
204
+ -- includes the step) — savers made a 2nd, organic one. Ratio-of-ratios
205
+ -- (nonsaver post/pre)/(saver post/pre) cancels window lengths +
206
+ -- acquisition ramp → ≈ 0.30 by knob. Savers engineered-small.
207
+ WITH born AS (SELECT uid, min(t) AS t0 FROM ev WHERE event = 'sign up' GROUP BY 1),
208
+ eligible AS (SELECT uid, t0 FROM born WHERE t0 <= TIMESTAMP '2026-05-01 23:59:59' - INTERVAL 35 DAY),
209
+ per AS (
210
+ SELECT b.uid,
211
+ count(e.uid) FILTER (WHERE e.event = 'save item' AND e.t <= b.t0 + INTERVAL 10 DAY) AS early_saves,
212
+ count(e.uid) FILTER (WHERE e.t <= b.t0 + INTERVAL 25 DAY) AS pre_events,
213
+ count(e.uid) FILTER (WHERE e.t > b.t0 + INTERVAL 25 DAY) AS post_events
214
+ FROM eligible b JOIN ev e ON e.uid = b.uid GROUP BY 1)
215
+ SELECT CASE WHEN early_saves >= 2 THEN 'saver' ELSE 'nonsaver' END AS cohort,
216
+ count(*) AS users, round(avg(pre_events), 1) AS avg_pre, round(avg(post_events), 1) AS avg_post,
217
+ round(avg(post_events)::DOUBLE / nullif(avg(pre_events), 0), 3) AS post_pre
218
+ FROM per GROUP BY 1 ORDER BY 1;
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ecommerce.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./ecommerce.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/ecommerce/ecommerce.js --data-prefix verify-ecommerce
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/ecommerce/ecommerce.js verify-ecommerce
12
+ * Run:
13
+ * node dungeons/vertical/ecommerce/ecommerce.verify.mjs [prefix] # default verify-ecommerce
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 './ecommerce.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-ecommerce';
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/ecommerce/ecommerce.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`ecommerce — 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) — the H8 story carries its own explicit
55
+ // 75-minute pairing window in SQL, 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);