@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,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);