@ak--47/dungeon-master 1.5.4 → 1.6.1
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/powertools/SKILL.md +75 -0
- package/.claude/skills/powertools/pt.mjs +91 -0
- package/.claude/skills/powertools/snapshot-project.mjs +124 -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 +224 -0
- package/HOOKS.md +566 -112
- package/README.md +30 -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 +7 -3
- 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 +151 -59
- 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 +298 -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,206 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- fitness.js — v1.6 human-inspection queries (DuckDB)
|
|
3
|
+
--
|
|
4
|
+
-- Every query is keyed to a story id in fitness.js's `stories` export;
|
|
5
|
+
-- the machine-checked verdicts come from:
|
|
6
|
+
-- node scripts/verify-stories.mjs dungeons/vertical/fitness/fitness.js --data-prefix verify-fitness
|
|
7
|
+
-- Generate first:
|
|
8
|
+
-- node scripts/verify-runner.mjs dungeons/vertical/fitness/fitness.js verify-fitness
|
|
9
|
+
-- Run this file:
|
|
10
|
+
-- duckdb -c ".read dungeons/vertical/fitness/fitness.sql"
|
|
11
|
+
-- ============================================================
|
|
12
|
+
|
|
13
|
+
-- ── identity-resolution prelude ─────────────────────────────
|
|
14
|
+
-- avgDevicePerUser: 3 + account 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-fitness-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-fitness-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 workout counts. H3/H4/H10 classify on counts taken after H8's
|
|
34
|
+
-- progress-checked drop and H5's resolver thinning; for non-resolver users
|
|
35
|
+
-- no later hook deletes "workout completed", so output counts rebuild the
|
|
36
|
+
-- hook cohorts exactly (resolver-sensitive queries exclude that segment).
|
|
37
|
+
CREATE OR REPLACE VIEW workout_ct AS
|
|
38
|
+
SELECT uid, count(*) AS w FROM ev WHERE event = 'workout completed' GROUP BY 1;
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
-- ── H1-morning-calorie-boost ────────────────────────────────
|
|
42
|
+
-- workouts 05:00-09:00 UTC carry calories_burned × 1.3. Nothing else
|
|
43
|
+
-- touches calories_burned, so avg AND median ratios read the knob.
|
|
44
|
+
SELECT CASE WHEN extract(hour FROM t) >= 5 AND extract(hour FROM t) < 9 THEN 'morning' ELSE 'other' END AS grp,
|
|
45
|
+
count(*) AS workouts, round(avg(calories_burned), 1) AS avg_cal, median(calories_burned) AS med_cal
|
|
46
|
+
FROM ev WHERE event = 'workout completed' GROUP BY 1 ORDER BY 1;
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
-- ── H2-ai-coaching-lift ─────────────────────────────────────
|
|
50
|
+
-- after day 35 (2026-02-05), workouts flip to coaching_mode='ai_assisted'
|
|
51
|
+
-- at 40% per-event, and ai_assisted duration × 1.2. Pre-launch ai rows
|
|
52
|
+
-- must be ZERO (declared pool is the single value 'self_guided').
|
|
53
|
+
SELECT count(*) FILTER (WHERE coaching_mode = 'ai_assisted' AND t <= TIMESTAMP '2026-02-05') AS pre_launch_ai,
|
|
54
|
+
count(*) FILTER (WHERE coaching_mode = 'ai_assisted') AS ai_total
|
|
55
|
+
FROM ev WHERE event IN ('workout completed', 'workout planned');
|
|
56
|
+
|
|
57
|
+
SELECT coaching_mode, count(*) AS workouts, round(avg(duration_minutes), 1) AS avg_dur
|
|
58
|
+
FROM ev WHERE event = 'workout completed' AND t > TIMESTAMP '2026-02-05' GROUP BY 1 ORDER BY 1;
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
-- ── H3-streak-achievements ──────────────────────────────────
|
|
62
|
+
-- ≥2-workout users: profile streak_days OVERWRITTEN to hook-time workout
|
|
63
|
+
-- count, plus C(w) = min(w−1, 3) + 4·max(w−4, 0) cloned achievements.
|
|
64
|
+
-- Contract is ONE-SIDED for non-resolvers: sd < w impossible (nothing adds
|
|
65
|
+
-- workouts after H3); sd > w happens when the silent future-time guard
|
|
66
|
+
-- deletes a counted workout post-hook (~0.5% of users) — so expect
|
|
67
|
+
-- below_w = 0 and eq_share ≥ 0.99, NOT perfect equality.
|
|
68
|
+
WITH j AS (SELECT u.distinct_id::VARCHAR AS uid, u.segment, u.streak_days AS sd, coalesce(w.w, 0) AS w
|
|
69
|
+
FROM users u LEFT JOIN workout_ct w ON w.uid = u.distinct_id::VARCHAR
|
|
70
|
+
WHERE u.segment <> 'resolver')
|
|
71
|
+
SELECT count(*) FILTER (WHERE w >= 2 AND sd < w) AS below_w,
|
|
72
|
+
count(*) FILTER (WHERE segment <> 'coach' AND sd = 1) AS unreachable_one,
|
|
73
|
+
round(count(*) FILTER (WHERE w >= 2 AND sd = w)::DOUBLE / nullif(count(*) FILTER (WHERE w >= 2), 0), 4) AS eq_share,
|
|
74
|
+
count(*) FILTER (WHERE w >= 2) AS streak_users
|
|
75
|
+
FROM j;
|
|
76
|
+
|
|
77
|
+
-- implied organic achievements (total − C(w)) — median should sit in the
|
|
78
|
+
-- low single digits (ach weight 2 of 68); a drifted clone formula would
|
|
79
|
+
-- push it negative or huge.
|
|
80
|
+
WITH ac AS (SELECT uid, count(*) AS a FROM ev WHERE event = 'achievement unlocked' GROUP BY 1),
|
|
81
|
+
j AS (SELECT u.distinct_id::VARCHAR AS uid, coalesce(w.w, 0) AS w, coalesce(a.a, 0) AS a
|
|
82
|
+
FROM users u LEFT JOIN workout_ct w ON w.uid = u.distinct_id::VARCHAR LEFT JOIN ac a ON a.uid = u.distinct_id::VARCHAR
|
|
83
|
+
WHERE u.segment <> 'resolver')
|
|
84
|
+
SELECT count(*) AS cohort,
|
|
85
|
+
round(count(*) FILTER (WHERE a - (LEAST(w - 1, 3) + GREATEST(w - 4, 0) * 4) >= 1)::DOUBLE / count(*), 4) AS ok_share,
|
|
86
|
+
median(a - (LEAST(w - 1, 3) + GREATEST(w - 4, 0) * 4)) AS med_organic
|
|
87
|
+
FROM j WHERE w BETWEEN 2 AND 14 AND a >= 1;
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
-- ── H4-social-challenge-completion ──────────────────────────
|
|
91
|
+
-- ≥3-friend users get max(1, floor(cc × 0.5)) cloned challenge completions.
|
|
92
|
+
-- out = cc + max(1, floor(cc/2)) skips {5, 8, 11, …} = {n≥5 : n≡2 mod 3} —
|
|
93
|
+
-- gap hits on the clean cohort only come from the future-time guard (~2%).
|
|
94
|
+
WITH fr AS (SELECT uid, count(*) AS f FROM ev WHERE event = 'friend added' GROUP BY 1),
|
|
95
|
+
ch AS (SELECT uid, count(*) AS c FROM ev WHERE event = 'challenge completed' GROUP BY 1),
|
|
96
|
+
j AS (SELECT u.distinct_id::VARCHAR AS uid, coalesce(w.w, 0) AS w, coalesce(f.f, 0) AS f, coalesce(c.c, 0) AS c
|
|
97
|
+
FROM users u LEFT JOIN workout_ct w ON w.uid = u.distinct_id::VARCHAR
|
|
98
|
+
LEFT JOIN fr f ON f.uid = u.distinct_id::VARCHAR LEFT JOIN ch c ON c.uid = u.distinct_id::VARCHAR
|
|
99
|
+
WHERE u.segment <> 'resolver')
|
|
100
|
+
SELECT count(*) AS clean_cohort, count(*) FILTER (WHERE c >= 5 AND c % 3 = 2) AS gap_hits
|
|
101
|
+
FROM j WHERE w <= 14 AND f >= 3 AND c >= 2;
|
|
102
|
+
|
|
103
|
+
-- within-social-segment gradient (composite: clone lift × activity correlation)
|
|
104
|
+
WITH fr AS (SELECT uid, count(*) AS f FROM ev WHERE event = 'friend added' GROUP BY 1),
|
|
105
|
+
ch AS (SELECT uid, count(*) AS c FROM ev WHERE event = 'challenge completed' GROUP BY 1),
|
|
106
|
+
j AS (SELECT u.distinct_id::VARCHAR AS uid, coalesce(f.f, 0) AS f, coalesce(c.c, 0) AS c
|
|
107
|
+
FROM users u LEFT JOIN fr f ON f.uid = u.distinct_id::VARCHAR LEFT JOIN ch c ON c.uid = u.distinct_id::VARCHAR
|
|
108
|
+
WHERE u.segment = 'social')
|
|
109
|
+
SELECT CASE WHEN f >= 3 THEN 'friend_heavy' ELSE 'friend_light' END AS grp,
|
|
110
|
+
count(*) AS users, round(avg(c), 3) AS avg_challenge_completions
|
|
111
|
+
FROM j WHERE f >= 3 OR f <= 1 GROUP BY 1 ORDER BY 1;
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
-- ── H5-resolver-churn-cliff ─────────────────────────────────
|
|
115
|
+
-- resolvers with <30 hook-time events lose 70% of post-day-14
|
|
116
|
+
-- (2026-01-15) events. Persona churnRate/activeWindow are deprecated
|
|
117
|
+
-- engine no-ops — the cliff is the hook alone, so the estimator targets
|
|
118
|
+
-- the 0.30 keep-rate. Deletions-only ⇒ eligible ⟺ output n < 30.
|
|
119
|
+
-- Double-difference: birth pinned to first event < day 2 (removes
|
|
120
|
+
-- birth-composition), lo/hi volume split normalized by the same split
|
|
121
|
+
-- inside casual (removes n-selection; casual lo/hi ≈ 1.22 measured).
|
|
122
|
+
-- DD = (res_lo/res_hi) ÷ (cas_lo/cas_hi) ≈ 0.30.
|
|
123
|
+
WITH tot AS (SELECT uid, count(*) AS n, min(t) AS first_t,
|
|
124
|
+
count(*) FILTER (WHERE t < TIMESTAMP '2026-01-15') AS pre,
|
|
125
|
+
count(*) FILTER (WHERE t >= TIMESTAMP '2026-01-15') AS post
|
|
126
|
+
FROM ev GROUP BY 1),
|
|
127
|
+
j AS (SELECT u.segment AS seg, CASE WHEN t.n < 30 THEN 'lo' ELSE 'hi' END AS arm, t.pre, t.post
|
|
128
|
+
FROM users u JOIN tot t ON t.uid = u.distinct_id::VARCHAR
|
|
129
|
+
WHERE t.first_t < TIMESTAMP '2026-01-03' AND u.segment IN ('resolver', 'casual'))
|
|
130
|
+
SELECT seg, arm, count(*) AS users,
|
|
131
|
+
round(sum(post)::DOUBLE / nullif(sum(pre), 0), 4) AS rho
|
|
132
|
+
FROM j GROUP BY 1, 2 ORDER BY 1, 2;
|
|
133
|
+
-- read: (rho[resolver,lo] / rho[resolver,hi]) / (rho[casual,lo] / rho[casual,hi])
|
|
134
|
+
-- ≈ 0.30; the casual lo/hi ratio itself is the placebo (~1, far from 0.3).
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
-- ── H6-coach-session-quality ────────────────────────────────
|
|
138
|
+
-- every coach-session satisfaction_score redrawn uniform [4.0, 5.0]
|
|
139
|
+
-- (unconditional — a single sub-4.0 score is a hook bug; avg = median = 4.5).
|
|
140
|
+
SELECT count(*) FILTER (WHERE satisfaction_score < 4.0) AS below_min,
|
|
141
|
+
count(*) AS sessions, round(avg(satisfaction_score), 3) AS avg_sat, median(satisfaction_score) AS med_sat
|
|
142
|
+
FROM ev WHERE event = 'coach session';
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
-- ── H7-coach-profile-enrichment ─────────────────────────────
|
|
146
|
+
-- user hook: coaches get total_workouts uniform [200, 500] (avg 350);
|
|
147
|
+
-- everyone else keeps the declared 0. Ranges deterministic. (streak_days
|
|
148
|
+
-- 60-365 also seeded here, but H3 overwrites it for ≥2-workout users —
|
|
149
|
+
-- total_workouts is the durable coach signature.)
|
|
150
|
+
SELECT CASE WHEN segment = 'coach' THEN 'coach' ELSE 'other' END AS grp,
|
|
151
|
+
count(*) AS users, min(total_workouts) AS min_tw, max(total_workouts) AS max_tw,
|
|
152
|
+
round(avg(total_workouts), 1) AS avg_tw
|
|
153
|
+
FROM users GROUP BY 1 ORDER BY 1;
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
-- ── H8-annual-follow-through ────────────────────────────────
|
|
157
|
+
-- 30% of free/monthly users lose ALL progress-checked events (per-user
|
|
158
|
+
-- cliff). Tier assigned BY segment → raw cross-tier reads are confounded
|
|
159
|
+
-- by persona composition BY CONSTRUCTION; only athlete and social contain
|
|
160
|
+
-- both an affected and a control tier, so standardize within those.
|
|
161
|
+
WITH pc AS (SELECT uid, count(*) AS ct FROM ev WHERE event = 'progress checked' GROUP BY 1),
|
|
162
|
+
j AS (SELECT u.segment AS seg, CASE WHEN u.subscription_tier IN ('annual', 'family') THEN 'ctl' ELSE 'aff' END AS arm,
|
|
163
|
+
coalesce(p.ct, 0) AS ct
|
|
164
|
+
FROM users u LEFT JOIN pc p ON p.uid = u.distinct_id::VARCHAR
|
|
165
|
+
WHERE u.segment IN ('athlete', 'social'))
|
|
166
|
+
SELECT seg, arm, count(*) AS users,
|
|
167
|
+
round(count(*) FILTER (WHERE ct = 0)::DOUBLE / count(*), 4) AS zero_share,
|
|
168
|
+
round(avg(ct) FILTER (WHERE ct > 0), 2) AS avg_ct_survivors
|
|
169
|
+
FROM j GROUP BY 1, 2 ORDER BY 1, 2;
|
|
170
|
+
-- read: (z_aff − z_ctl) / (1 − z_ctl) per segment ≈ 0.30; survivor avgs
|
|
171
|
+
-- match within segment (per-user cliff, not thinning).
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
-- ── H9-workout-loop-ttc ─────────────────────────────────────
|
|
175
|
+
-- funnel-post scales EVERY funnel instance's gaps by tier: annual/family
|
|
176
|
+
-- × 0.77, free × 1.25, monthly = 1.0.
|
|
177
|
+
-- CAUTION: cross-event TTC SQL here would be censored by its lookback
|
|
178
|
+
-- window (the ai-platform H9 lesson — censoring can invert the measured
|
|
179
|
+
-- direction). The story asserts TTC through the Mixpanel-aligned emulator
|
|
180
|
+
-- at a 60h window (= 48h generative window × 1.25 max stretch, covering
|
|
181
|
+
-- the stretched support); trust the story verdict, not ad-hoc pair SQL.
|
|
182
|
+
SELECT subscription_tier, count(*) AS planned_events
|
|
183
|
+
FROM ev WHERE event = 'workout planned' GROUP BY 1 ORDER BY 1;
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
-- ── H10-workout-magic-number ────────────────────────────────
|
|
187
|
+
-- sweet 12-14 workouts → ALL workout durations × 1.35; over 15+ → 65% of
|
|
188
|
+
-- post-day-30 (2026-01-31) events dropped EXCEPT workout completed /
|
|
189
|
+
-- progress checked. Duration read restricted to pre-AI-launch (≤2026-02-05)
|
|
190
|
+
-- where H2 never touched durations — AVG ratio reads the 1.35 knob exactly;
|
|
191
|
+
-- median atom-snaps on the discrete duration pool (reads high, ~1.48).
|
|
192
|
+
WITH coh AS (SELECT uid, CASE WHEN w BETWEEN 12 AND 14 THEN 'sweet'
|
|
193
|
+
WHEN w BETWEEN 2 AND 11 THEN 'low' WHEN w >= 15 THEN 'over' END AS grp FROM workout_ct)
|
|
194
|
+
SELECT c.grp, count(DISTINCT c.uid) AS users, round(avg(e.duration_minutes), 2) AS avg_dur_prelaunch
|
|
195
|
+
FROM coh c JOIN ev e ON e.uid = c.uid AND e.event = 'workout completed' AND e.t <= TIMESTAMP '2026-02-05'
|
|
196
|
+
WHERE c.grp IS NOT NULL GROUP BY 1 ORDER BY avg_dur_prelaunch;
|
|
197
|
+
|
|
198
|
+
-- over-drop: within-user-normalized double ratio of non-preserved volume
|
|
199
|
+
WITH coh AS (SELECT uid, CASE WHEN w >= 15 THEN 'over' WHEN w BETWEEN 12 AND 14 THEN 'sweet' END AS grp
|
|
200
|
+
FROM workout_ct WHERE w >= 12),
|
|
201
|
+
per AS (SELECT c.grp, c.uid,
|
|
202
|
+
count(*) FILTER (WHERE e.event NOT IN ('workout completed', 'progress checked') AND e.t < TIMESTAMP '2026-01-31') AS pre,
|
|
203
|
+
count(*) FILTER (WHERE e.event NOT IN ('workout completed', 'progress checked') AND e.t >= TIMESTAMP '2026-01-31') AS post
|
|
204
|
+
FROM coh c JOIN ev e ON e.uid = c.uid GROUP BY 1, 2)
|
|
205
|
+
SELECT grp, count(*) AS users, round(sum(post)::DOUBLE / nullif(sum(pre), 0), 4) AS post_pre
|
|
206
|
+
FROM per WHERE grp IS NOT NULL GROUP BY 1 ORDER BY 1;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fitness.verify.mjs — thin wrapper around the story runner.
|
|
4
|
+
*
|
|
5
|
+
* All verification logic lives in the `stories` export of ./fitness.js;
|
|
6
|
+
* this script just streams the shards and delegates. It is equivalent to:
|
|
7
|
+
*
|
|
8
|
+
* node scripts/verify-stories.mjs dungeons/vertical/fitness/fitness.js --data-prefix verify-fitness
|
|
9
|
+
*
|
|
10
|
+
* Generate first:
|
|
11
|
+
* node scripts/verify-runner.mjs dungeons/vertical/fitness/fitness.js verify-fitness
|
|
12
|
+
* Run:
|
|
13
|
+
* node dungeons/vertical/fitness/fitness.verify.mjs [prefix] # default verify-fitness
|
|
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 './fitness.js';
|
|
22
|
+
|
|
23
|
+
const PREFIX = process.argv[2] || 'verify-fitness';
|
|
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/fitness/fitness.js ${PREFIX}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(`fitness — 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 (48h generative window × the 1.25 max stretch factor,
|
|
56
|
+
// covering the stretched support), so no funnel-default threading is needed
|
|
57
|
+
const results = await evaluateStories(stories, events, {
|
|
58
|
+
profiles,
|
|
59
|
+
funnels: config.funnels,
|
|
60
|
+
identityMap: buildIdentityMap(profiles),
|
|
61
|
+
runSql,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let worst = 'NAILED';
|
|
65
|
+
for (const r of results) {
|
|
66
|
+
console.log(`${r.verdict.padEnd(7)} ${r.id}`);
|
|
67
|
+
for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
|
|
68
|
+
if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
|
|
69
|
+
}
|
|
70
|
+
console.log(`\nworst verdict: ${worst}`);
|
|
71
|
+
process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);
|