@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.
- 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 +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- 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 +183 -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 +2 -1
- 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,288 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- dating.js — v1.6 human-inspection queries (DuckDB)
|
|
3
|
+
--
|
|
4
|
+
-- Every query is keyed to a story id in dating.js's `stories` export;
|
|
5
|
+
-- the machine-checked verdicts come from:
|
|
6
|
+
-- node scripts/verify-stories.mjs dungeons/vertical/dating/dating.js --data-prefix verify-dating
|
|
7
|
+
-- Generate first:
|
|
8
|
+
-- node scripts/verify-runner.mjs dungeons/vertical/dating/dating.js verify-dating
|
|
9
|
+
-- Run this file:
|
|
10
|
+
-- duckdb -c ".read dungeons/vertical/dating/dating.sql"
|
|
11
|
+
-- ============================================================
|
|
12
|
+
|
|
13
|
+
-- ── identity-resolution prelude ─────────────────────────────
|
|
14
|
+
-- avgDevicePerUser: 2 + profile created is both isAuthEvent and
|
|
15
|
+
-- isFirstEvent, so born users auth on their first event; the device-pool
|
|
16
|
+
-- resolve is belt-and-braces for any device-only edge.
|
|
17
|
+
CREATE OR REPLACE VIEW users AS
|
|
18
|
+
SELECT * FROM read_json_auto('data/verify-dating-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
|
+
SELECT unnest("anonymousIds") AS device_id, distinct_id FROM users;
|
|
23
|
+
|
|
24
|
+
CREATE OR REPLACE VIEW ev AS
|
|
25
|
+
-- ::VARCHAR casts — user_id sniffs as UUID, device_id as VARCHAR; DuckDB
|
|
26
|
+
-- refuses to coalesce mixed types
|
|
27
|
+
SELECT coalesce(m.distinct_id::VARCHAR, e.user_id::VARCHAR, e.device_id::VARCHAR) AS uid,
|
|
28
|
+
e.time::TIMESTAMP AS t,
|
|
29
|
+
e.*
|
|
30
|
+
FROM read_json_auto('data/verify-dating-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
31
|
+
LEFT JOIN device_map m ON e.device_id = m.device_id;
|
|
32
|
+
|
|
33
|
+
-- Per-user counts. Deletions-only hooks (H5 ghosting, H8 off-app drop,
|
|
34
|
+
-- future-time guard) make hook-time cohort recovery ONE-SIDED: output
|
|
35
|
+
-- count >= threshold implies hook-time count >= threshold, but not the
|
|
36
|
+
-- reverse. Stories handle this with output-implies-hook cohort choices;
|
|
37
|
+
-- read these tables the same way.
|
|
38
|
+
CREATE OR REPLACE VIEW per_user AS
|
|
39
|
+
SELECT uid,
|
|
40
|
+
count(*) FILTER (WHERE event = 'photo uploaded') AS photos,
|
|
41
|
+
count(*) FILTER (WHERE event = 'match received') AS matches,
|
|
42
|
+
count(*) FILTER (WHERE event = 'swipe right') AS swipes,
|
|
43
|
+
count(*) FILTER (WHERE event = 'swipe right' AND is_super_like = true) AS sls,
|
|
44
|
+
count(*) FILTER (WHERE event = 'message sent') AS msgs,
|
|
45
|
+
count(*) FILTER (WHERE event = 'date scheduled') AS dates
|
|
46
|
+
FROM ev GROUP BY 1;
|
|
47
|
+
|
|
48
|
+
-- output-visible timely pair (match → message within 48h) proves the user
|
|
49
|
+
-- was NOT ghosted at hook time (H5 deletes but never adds)
|
|
50
|
+
CREATE OR REPLACE VIEW timely AS
|
|
51
|
+
SELECT DISTINCT a.uid FROM ev a
|
|
52
|
+
JOIN ev b ON b.uid = a.uid AND b.event = 'message sent'
|
|
53
|
+
WHERE a.event = 'match received' AND b.t > a.t AND b.t < a.t + INTERVAL 48 HOUR;
|
|
54
|
+
|
|
55
|
+
-- output-visible early milestone (phone/date inside first 14 days)
|
|
56
|
+
CREATE OR REPLACE VIEW first_ev AS
|
|
57
|
+
SELECT uid, min(t) AS f FROM ev GROUP BY 1;
|
|
58
|
+
|
|
59
|
+
CREATE OR REPLACE VIEW milestone AS
|
|
60
|
+
SELECT DISTINCT e.uid FROM ev e JOIN first_ev fe ON fe.uid = e.uid
|
|
61
|
+
WHERE e.event IN ('phone number exchanged', 'date scheduled')
|
|
62
|
+
AND e.t < fe.f + INTERVAL 14 DAY;
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
-- ── H1-photo-magic-number ───────────────────────────────────
|
|
66
|
+
-- score cut: 6+ uploaders' match_score × 0.65 applied at the END of the
|
|
67
|
+
-- everything hook (covers H3/H4-injected matches too). Compare vs 0-1
|
|
68
|
+
-- uploaders, NOT vs sweet (sweet users' H1 clones redraw score U[60,98]).
|
|
69
|
+
SELECT CASE WHEN p.photos >= 6 THEN 'over' WHEN p.photos <= 1 THEN 'low' ELSE 'sweet' END AS grp,
|
|
70
|
+
count(DISTINCT p.uid) AS users, round(avg(e.match_score), 2) AS avg_score
|
|
71
|
+
FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'match received'
|
|
72
|
+
GROUP BY 1 ORDER BY 1;
|
|
73
|
+
-- read: over/low avg_score ≈ 0.65 (knob); sweet reads high (clone redraws)
|
|
74
|
+
|
|
75
|
+
-- count lift: activity-normalized double ratio, Free tier, H3's additive
|
|
76
|
+
-- term subtracted arithmetically (adj = matches − 3·super_likes).
|
|
77
|
+
-- Conditioning on sls=0 instead would select the near-inactive tail
|
|
78
|
+
-- (P(no SL) ≈ 0.9^swipes) and starve the sweet cell. DD ≈ 1+E[U{2..4}] = 4.
|
|
79
|
+
SELECT CASE WHEN p.photos BETWEEN 2 AND 5 THEN 'sweet' WHEN p.photos <= 1 THEN 'low' END AS arm,
|
|
80
|
+
count(*) AS users, round(avg(p.matches - 3 * p.sls), 3) AS avg_adj_m, round(avg(p.swipes), 3) AS avg_s
|
|
81
|
+
FROM per_user p JOIN users u ON u.distinct_id::VARCHAR = p.uid
|
|
82
|
+
WHERE u.subscription = 'Free' AND p.swipes > 0
|
|
83
|
+
AND (p.photos BETWEEN 2 AND 5 OR p.photos <= 1)
|
|
84
|
+
GROUP BY 1 ORDER BY 1;
|
|
85
|
+
-- read: (avg_adj_m_sweet/avg_adj_m_low) ÷ (avg_s_sweet/avg_s_low) ≈ 4
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
-- ── H2-sunday-swipe-surge ───────────────────────────────────
|
|
89
|
+
-- Sunday swipes cloned in place: evening (18-23 UTC) ×6, daytime ×3.
|
|
90
|
+
-- dayofweek(): Sunday = 0 (matches JS getUTCDay).
|
|
91
|
+
SELECT dayofweek(t) AS dow, count(*) AS swipes
|
|
92
|
+
FROM ev WHERE event = 'swipe right' GROUP BY 1 ORDER BY 1;
|
|
93
|
+
-- read: dow 0 strict max; Sunday / mean(other six) in [2, 6]
|
|
94
|
+
|
|
95
|
+
-- hour-of-day mix inside Sunday (evening share drives where the
|
|
96
|
+
-- multiplier lands between 3 and 6)
|
|
97
|
+
SELECT CASE WHEN extract(hour FROM t) >= 18 THEN 'evening' ELSE 'daytime' END AS bucket,
|
|
98
|
+
count(*) AS sunday_swipes
|
|
99
|
+
FROM ev WHERE event = 'swipe right' AND dayofweek(t) = 0 GROUP BY 1 ORDER BY 1;
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
-- ── H3-super-like-effect ────────────────────────────────────
|
|
103
|
+
-- each super-like injects exactly 3 cloned matches (additive, scores
|
|
104
|
+
-- U[70,99]). Cohort: Free, photos outside sweet 2-5 (isolates from H1/H4).
|
|
105
|
+
SELECT CASE WHEN p.sls >= 1 THEN 'super_liker' ELSE 'no_sl' END AS arm,
|
|
106
|
+
count(*) AS users, round(avg(p.matches), 3) AS avg_m,
|
|
107
|
+
round(avg(p.swipes), 3) AS avg_s, round(avg(p.sls), 3) AS avg_sl
|
|
108
|
+
FROM per_user p JOIN users u ON u.distinct_id::VARCHAR = p.uid
|
|
109
|
+
WHERE u.subscription = 'Free' AND p.photos NOT BETWEEN 2 AND 5 AND p.swipes > 0
|
|
110
|
+
GROUP BY 1 ORDER BY 1;
|
|
111
|
+
-- read: measured lift avg_m(sl)/[avg_m(none) × avg_s(sl)/avg_s(none)]
|
|
112
|
+
-- vs predicted (organic + 3·avg_sl)/organic — ratio ≈ 1
|
|
113
|
+
|
|
114
|
+
-- injected-match score floor: H3 clones draw match_score U[70,99]; for
|
|
115
|
+
-- non-over-6 users nothing lowers scores, so super-liker matches skew high
|
|
116
|
+
SELECT CASE WHEN p.sls >= 1 THEN 'super_liker' ELSE 'no_sl' END AS arm,
|
|
117
|
+
round(avg(e.match_score), 2) AS avg_score, median(e.match_score) AS med_score
|
|
118
|
+
FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'match received'
|
|
119
|
+
WHERE p.photos < 6 GROUP BY 1 ORDER BY 1;
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
-- ── H4-premium-match-boost ──────────────────────────────────
|
|
123
|
+
-- H4 runs after H5's churn: surviving matches × 2 (Premium) / × 4 (Elite),
|
|
124
|
+
-- toAdd = base×mult − base exactly. Tier ⊥ activity → cross-tier avg ratio
|
|
125
|
+
-- reads the multiplier (diluted only by zero-hook-match users).
|
|
126
|
+
SELECT u.subscription AS tier, count(*) AS users,
|
|
127
|
+
round(avg(coalesce(p.matches, 0)), 3) AS avg_matches
|
|
128
|
+
FROM users u LEFT JOIN per_user p ON p.uid = u.distinct_id::VARCHAR
|
|
129
|
+
GROUP BY 1 ORDER BY avg_matches;
|
|
130
|
+
-- read: Elite/Free ≈ 4, Premium/Free ≈ 2 (both slightly diluted)
|
|
131
|
+
|
|
132
|
+
-- structural signature: timely ∩ milestone Elite users (non-ghosted, H8
|
|
133
|
+
-- add-branch) have matches ≡ 0 mod 4 except the ~1-2% future-guard tail;
|
|
134
|
+
-- Free is the placebo (~0.25 random).
|
|
135
|
+
SELECT u.subscription AS tier, count(*) AS users,
|
|
136
|
+
round(count(*) FILTER (WHERE p.matches % 4 = 0)::DOUBLE / count(*), 4) AS mod4_share
|
|
137
|
+
FROM users u
|
|
138
|
+
JOIN per_user p ON p.uid = u.distinct_id::VARCHAR
|
|
139
|
+
JOIN timely tp ON tp.uid = p.uid
|
|
140
|
+
JOIN milestone ms ON ms.uid = p.uid
|
|
141
|
+
WHERE p.matches >= 4 AND u.subscription IN ('Elite', 'Free')
|
|
142
|
+
GROUP BY 1 ORDER BY 1;
|
|
143
|
+
-- read: Elite mod4_share ≥ 0.9, Free ≈ 0.25
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
-- ── H5-ghosting-churn ───────────────────────────────────────
|
|
147
|
+
-- no timely message within 48h of any match → 80% of post-first-match
|
|
148
|
+
-- events dropped (keep 0.2). Non-milestone restriction: H8's post-day-30
|
|
149
|
+
-- drop applies to BOTH arms and cancels in the ρ ratio (milestone users
|
|
150
|
+
-- get H8 ADDS, which would inflate the timely arm only).
|
|
151
|
+
-- The raw ρ ratio is confounded DOWNWARD by activity selection (the
|
|
152
|
+
-- ghosted arm is the least-engaged matched tail on a flatter organic
|
|
153
|
+
-- trajectory), so the story self-calibrates on the PRE-first-match
|
|
154
|
+
-- half-split (rho_pre) — H5 never touches pre-match events.
|
|
155
|
+
WITH fm AS (SELECT uid, min(t) AS first_match FROM ev WHERE event = 'match received' GROUP BY 1),
|
|
156
|
+
per AS (
|
|
157
|
+
SELECT fm.uid,
|
|
158
|
+
count(*) FILTER (WHERE e.t <= fm.first_match) AS pre,
|
|
159
|
+
count(*) FILTER (WHERE e.t > fm.first_match) AS post,
|
|
160
|
+
count(*) FILTER (WHERE e.t <= to_timestamp((epoch(fe.f) + epoch(fm.first_match)) / 2)) AS pre_a,
|
|
161
|
+
count(*) FILTER (WHERE e.t > to_timestamp((epoch(fe.f) + epoch(fm.first_match)) / 2) AND e.t <= fm.first_match) AS pre_b
|
|
162
|
+
FROM fm JOIN first_ev fe ON fe.uid = fm.uid JOIN ev e ON e.uid = fm.uid GROUP BY 1
|
|
163
|
+
)
|
|
164
|
+
SELECT CASE WHEN tp.uid IS NOT NULL THEN 'timely' ELSE 'ghosted' END AS arm,
|
|
165
|
+
count(*) AS users, round(sum(post)::DOUBLE / nullif(sum(pre), 0), 4) AS rho,
|
|
166
|
+
round(sum(pre_b)::DOUBLE / nullif(sum(pre_a), 0), 4) AS rho_pre
|
|
167
|
+
FROM per LEFT JOIN timely tp ON tp.uid = per.uid
|
|
168
|
+
WHERE per.uid NOT IN (SELECT uid FROM milestone)
|
|
169
|
+
GROUP BY 1 ORDER BY 1;
|
|
170
|
+
-- read: (rho_g/rho_t) ÷ (rho_pre_g/rho_pre_t) ≈ 0.2 keep rate (±40%)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
-- ── H6-bio-prompt-power-users ───────────────────────────────
|
|
174
|
+
-- bio ≥1 ∧ prompts ≥3 → 3 cloned dates per existing date (×4 at hook
|
|
175
|
+
-- time). At ~190 events/user the power cohort is ~80% of users; the rest
|
|
176
|
+
-- arm is the low-activity tail whose messages come disproportionately
|
|
177
|
+
-- from Date Funnel instances (which co-emit dates), so its ORGANIC
|
|
178
|
+
-- dates-per-message runs ~2× the power arm's. Population rate ratio is a
|
|
179
|
+
-- composite: 4× mechanism × 0.4-0.85 composition → [1.5, 3.4].
|
|
180
|
+
WITH pw AS (
|
|
181
|
+
SELECT uid,
|
|
182
|
+
count(*) FILTER (WHERE event = 'bio updated') AS bios,
|
|
183
|
+
count(*) FILTER (WHERE event = 'prompt answered') AS prompts,
|
|
184
|
+
count(*) FILTER (WHERE event = 'date scheduled') AS dates,
|
|
185
|
+
count(*) FILTER (WHERE event = 'message sent') AS msgs
|
|
186
|
+
FROM ev GROUP BY 1
|
|
187
|
+
)
|
|
188
|
+
SELECT CASE WHEN bios >= 1 AND prompts >= 3 THEN 'power' ELSE 'rest' END AS arm,
|
|
189
|
+
count(*) AS users, round(sum(dates)::DOUBLE / sum(msgs), 5) AS date_rate
|
|
190
|
+
FROM pw WHERE msgs > 0 GROUP BY 1 ORDER BY 1;
|
|
191
|
+
-- read: power/rest date_rate in [1.5, 3.4] (composite, see above)
|
|
192
|
+
|
|
193
|
+
-- exact mechanism: timely ∩ milestone power users have no post-H6 date
|
|
194
|
+
-- deletions → output dates ≡ 0 (mod 4) except the ~9% future-guard tail
|
|
195
|
+
-- (H6 clones stamped source+1..72h past datasetEnd silently dropped);
|
|
196
|
+
-- clean-cohort rest arm is the placebo (~0.25 random).
|
|
197
|
+
WITH pw AS (
|
|
198
|
+
SELECT uid,
|
|
199
|
+
count(*) FILTER (WHERE event = 'bio updated') AS bios,
|
|
200
|
+
count(*) FILTER (WHERE event = 'prompt answered') AS prompts,
|
|
201
|
+
count(*) FILTER (WHERE event = 'date scheduled') AS dates
|
|
202
|
+
FROM ev GROUP BY 1
|
|
203
|
+
)
|
|
204
|
+
SELECT CASE WHEN p.bios >= 1 AND p.prompts >= 3 THEN 'power' ELSE 'rest' END AS arm,
|
|
205
|
+
count(*) AS users,
|
|
206
|
+
round(count(*) FILTER (WHERE p.dates % 4 = 0)::DOUBLE / count(*), 4) AS mod4_share
|
|
207
|
+
FROM pw p
|
|
208
|
+
JOIN timely tp ON tp.uid = p.uid
|
|
209
|
+
JOIN milestone ms ON ms.uid = p.uid
|
|
210
|
+
WHERE p.dates >= 4 GROUP BY 1 ORDER BY 1;
|
|
211
|
+
-- read: power mod4_share ≥ 0.9, rest ≈ 0.25
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
-- ── H7-vday-spike ───────────────────────────────────────────
|
|
215
|
+
-- V-Day window = dataset days 58-63 (2026-02-28 → 2026-03-05): signups
|
|
216
|
+
-- ×3 total (clones +U[1,48]h, E[leak] 20%), upgrades ×5 total (clones
|
|
217
|
+
-- +U[1,24]h, E[leak] 10%). Baseline flanks skip 3 days post-window so
|
|
218
|
+
-- clone spill can't inflate them.
|
|
219
|
+
SELECT date_trunc('day', t) AS day, count(*) AS signups
|
|
220
|
+
FROM ev WHERE event = 'profile created'
|
|
221
|
+
AND t >= TIMESTAMP '2026-02-21' AND t < TIMESTAMP '2026-03-12'
|
|
222
|
+
GROUP BY 1 ORDER BY 1;
|
|
223
|
+
-- read: 02-28..03-04 daily ≈ 2.6× the flanking days
|
|
224
|
+
|
|
225
|
+
SELECT
|
|
226
|
+
count(*) FILTER (WHERE t >= TIMESTAMP '2026-02-28' AND t < TIMESTAMP '2026-03-05') / 5.0 AS window_daily,
|
|
227
|
+
count(*) FILTER (WHERE (t >= TIMESTAMP '2026-02-14' AND t < TIMESTAMP '2026-02-28')
|
|
228
|
+
OR (t >= TIMESTAMP '2026-03-08' AND t < TIMESTAMP '2026-03-22')) / 28.0 AS baseline_daily
|
|
229
|
+
FROM ev WHERE event = 'premium upgrade';
|
|
230
|
+
-- read: window/baseline ≈ 4.6 (band [3.5, 5.6])
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
-- ── H8-offapp-retention ─────────────────────────────────────
|
|
234
|
+
-- milestone users (early phone/date) get post-day-30 top-up clones toward
|
|
235
|
+
-- 30% share; non-milestone lose 80% of post-day-30 events. Cohort: born
|
|
236
|
+
-- before day 30 (clone support day30+U[1,60] fits the 121-day window) AND
|
|
237
|
+
-- timely-or-match-free (removes the H5 confound).
|
|
238
|
+
WITH per AS (
|
|
239
|
+
SELECT fe.uid,
|
|
240
|
+
count(*) FILTER (WHERE e.t > fe.f + INTERVAL 30 DAY) AS post30,
|
|
241
|
+
count(*) AS total
|
|
242
|
+
FROM first_ev fe JOIN ev e ON e.uid = fe.uid
|
|
243
|
+
WHERE fe.f < TIMESTAMP '2026-01-31' GROUP BY 1
|
|
244
|
+
)
|
|
245
|
+
SELECT CASE WHEN ms.uid IS NOT NULL THEN 'milestone' ELSE 'rest' END AS arm,
|
|
246
|
+
count(*) AS users, round(sum(post30)::DOUBLE / sum(total), 4) AS post30_share
|
|
247
|
+
FROM per
|
|
248
|
+
LEFT JOIN milestone ms ON ms.uid = per.uid
|
|
249
|
+
LEFT JOIN timely tp ON tp.uid = per.uid
|
|
250
|
+
LEFT JOIN per_user pu ON pu.uid = per.uid
|
|
251
|
+
WHERE tp.uid IS NOT NULL OR coalesce(pu.matches, 0) = 0
|
|
252
|
+
GROUP BY 1 ORDER BY 1;
|
|
253
|
+
-- read: with keep k=0.2 and milestone share s (≈ organic), predicted rest
|
|
254
|
+
-- share = k·s / (1 − (1−k)·s); measured rest / predicted ≈ 1
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
-- ── H9-match-flow-ttc ───────────────────────────────────────
|
|
258
|
+
-- funnel-post stretches Match Flow gaps by tier: Elite ×0.71, Free ×1.4,
|
|
259
|
+
-- Premium untouched (v1.6 scopes the hook to Match Flow only).
|
|
260
|
+
-- CAUTION: cross-event TTC SQL here is the documented greedy-single-pass
|
|
261
|
+
-- limitation — it pairs swipes/matches across funnel instances and buries
|
|
262
|
+
-- the signal. The story asserts TTC through the Mixpanel-aligned emulator
|
|
263
|
+
-- (timeToConvert, 33.6h window = 24h generative × 1.4 max stretch); trust
|
|
264
|
+
-- the story verdict, not ad-hoc pair SQL.
|
|
265
|
+
SELECT u.subscription AS tier, count(*) AS matches
|
|
266
|
+
FROM ev e JOIN users u ON u.distinct_id::VARCHAR = e.uid
|
|
267
|
+
WHERE e.event = 'match received' GROUP BY 1 ORDER BY 1;
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
-- ── H10-age-date-conversion ─────────────────────────────────
|
|
271
|
+
-- funnel-pre scales Date Funnel completion: 25-29/30-34 ×1.3, 40+ ×0.6.
|
|
272
|
+
-- Emulator story reads step_counts at the 72h generative window; this
|
|
273
|
+
-- query approximates the same read with a sequenced 72h pairing per user
|
|
274
|
+
-- (close enough for eyeballing, not for the verdict).
|
|
275
|
+
WITH msg AS (SELECT uid, min(t) AS m0 FROM ev WHERE event = 'message sent' GROUP BY 1),
|
|
276
|
+
done AS (
|
|
277
|
+
SELECT DISTINCT m.uid FROM msg m
|
|
278
|
+
JOIN ev d ON d.uid = m.uid AND d.event = 'date scheduled'
|
|
279
|
+
WHERE d.t > m.m0 AND d.t < m.m0 + INTERVAL 72 HOUR
|
|
280
|
+
)
|
|
281
|
+
SELECT u.age_range, count(*) AS msg_users,
|
|
282
|
+
round(count(*) FILTER (WHERE dn.uid IS NOT NULL)::DOUBLE / count(*), 4) AS date_rate_72h
|
|
283
|
+
FROM msg m
|
|
284
|
+
JOIN users u ON u.distinct_id::VARCHAR = m.uid
|
|
285
|
+
LEFT JOIN done dn ON dn.uid = m.uid
|
|
286
|
+
GROUP BY 1 ORDER BY 1;
|
|
287
|
+
-- read: 25-29/30-34 rates > 18-24/35-39 > 40+ (compressed vs the
|
|
288
|
+
-- 1.3/0.6 knobs by organic age-independent dates)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dating.verify.mjs — thin wrapper around the story runner.
|
|
4
|
+
*
|
|
5
|
+
* All verification logic lives in the `stories` export of ./dating.js;
|
|
6
|
+
* this script just streams the shards and delegates. It is equivalent to:
|
|
7
|
+
*
|
|
8
|
+
* node scripts/verify-stories.mjs dungeons/vertical/dating/dating.js --data-prefix verify-dating
|
|
9
|
+
*
|
|
10
|
+
* Generate first:
|
|
11
|
+
* node scripts/verify-runner.mjs dungeons/vertical/dating/dating.js verify-dating
|
|
12
|
+
* Run:
|
|
13
|
+
* node dungeons/vertical/dating/dating.verify.mjs [prefix] # default verify-dating
|
|
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 './dating.js';
|
|
22
|
+
|
|
23
|
+
const PREFIX = process.argv[2] || 'verify-dating';
|
|
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/dating/dating.js ${PREFIX}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(`dating — 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 H9 stories carry their own explicit
|
|
55
|
+
// conversion window (24h Match Flow generative window × the 1.4 max stretch
|
|
56
|
+
// factor = 33.6h, covering the stretched support), so no funnel-default
|
|
57
|
+
// threading is needed
|
|
58
|
+
const results = await evaluateStories(stories, events, {
|
|
59
|
+
profiles,
|
|
60
|
+
funnels: config.funnels,
|
|
61
|
+
identityMap: buildIdentityMap(profiles),
|
|
62
|
+
runSql,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let worst = 'NAILED';
|
|
66
|
+
for (const r of results) {
|
|
67
|
+
console.log(`${r.verdict.padEnd(7)} ${r.id}`);
|
|
68
|
+
for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
|
|
69
|
+
if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
|
|
70
|
+
}
|
|
71
|
+
console.log(`\nworst verdict: ${worst}`);
|
|
72
|
+
process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);
|