@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.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +3 -2
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- 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);
|