@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.
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 +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  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 +183 -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 +2 -1
  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,216 @@
1
+ -- ============================================================
2
+ -- community.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in community.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/community/community.js --data-prefix verify-community
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/community/community.js verify-community
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/community/community.sql"
11
+ -- ============================================================
12
+
13
+ -- ── identity-resolution prelude ─────────────────────────────
14
+ -- avgDevicePerUser: 2 + 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-community-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-community-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 (H6 lurker churn, H8 free-tier
34
+ -- comment drop, H10 over-publisher upvote drop, future-time guard) make
35
+ -- hook-time cohort recovery ONE-SIDED: output count >= threshold implies
36
+ -- hook-time count >= threshold, but not the reverse. Stories handle this
37
+ -- with output-implies-hook cohort choices; read these tables the same way.
38
+ CREATE OR REPLACE VIEW per_user AS
39
+ SELECT uid,
40
+ count(*) FILTER (WHERE event = 'article published') AS pubs,
41
+ count(*) FILTER (WHERE event = 'article edited') AS edits,
42
+ count(*) FILTER (WHERE event = 'comment posted') AS comments,
43
+ count(*) FILTER (WHERE event = 'upvote given') AS upvotes,
44
+ count(*) FILTER (WHERE event = 'app session') AS sessions,
45
+ count(*) FILTER (WHERE event = 'discussion posted') AS discussions,
46
+ count(*) AS n
47
+ FROM ev GROUP BY 1;
48
+
49
+ CREATE OR REPLACE VIEW first_ev AS
50
+ SELECT uid, min(t) AS f FROM ev GROUP BY 1;
51
+
52
+
53
+ -- ── H1-weekend-word-count ───────────────────────────────────
54
+ -- Sat/Sun 'article published' word_count ×1.5 (floored). 'wiki page
55
+ -- created' carries the same prop but is untouched — the placebo arm.
56
+ -- dayofweek(): Sunday = 0 (matches JS getUTCDay).
57
+ SELECT event,
58
+ CASE WHEN dayofweek(t) IN (0, 6) THEN 'weekend' ELSE 'weekday' END AS bucket,
59
+ count(*) AS n, round(avg(word_count), 1) AS avg_wc
60
+ FROM ev WHERE event IN ('article published', 'wiki page created')
61
+ GROUP BY 1, 2 ORDER BY 1, 2;
62
+ -- read: published weekend/weekday ≈ 1.5; wiki ≈ 1.0
63
+
64
+
65
+ -- ── H2-trending-gaming-window ───────────────────────────────
66
+ -- days 35-50 EXCLUSIVE (hook uses isAfter/isBefore): gaming-hub users'
67
+ -- 'article viewed' view_count ×2. Event content_hub is stamped from the
68
+ -- profile BEFORE H2 runs, so it selects exactly the treated users.
69
+ SELECT CASE WHEN content_hub = 'gaming' THEN 'gaming' ELSE 'other' END AS hub,
70
+ CASE WHEN t > TIMESTAMP '2026-02-05' AND t < TIMESTAMP '2026-02-20' THEN 'in_window' ELSE 'outside' END AS bucket,
71
+ count(*) AS n, round(avg(view_count), 1) AS avg_vc
72
+ FROM ev WHERE event = 'article viewed'
73
+ GROUP BY 1, 2 ORDER BY 1, 2;
74
+ -- read: gaming in/out ≈ 2.0; other in/out ≈ 1.0
75
+
76
+
77
+ -- ── H3-power-creator-upvotes ────────────────────────────────
78
+ -- >20 publishes at hook time → upvote_count ×3 (floor(3w)=3w exact on
79
+ -- integers). Publishes never deleted → output ≥21 IMPLIES treatment.
80
+ SELECT CASE WHEN p.pubs > 20 THEN 'power' WHEN p.pubs <= 1 THEN 'low' END AS arm,
81
+ count(DISTINCT p.uid) AS users, round(avg(e.upvote_count), 2) AS avg_uc,
82
+ round(count(*) FILTER (WHERE e.upvote_count % 3 = 0)::DOUBLE / count(*), 4) AS mod3_share
83
+ FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'upvote given'
84
+ WHERE p.pubs > 20 OR p.pubs <= 1
85
+ GROUP BY 1 ORDER BY 1;
86
+ -- read: power/low avg_uc ≈ 3.0; power mod3_share ≈ 1.0, low ≈ 0.2-0.35
87
+
88
+
89
+ -- ── H4-discussion-depth ─────────────────────────────────────
90
+ -- active_contributor (role 'contributor') clones 50% of surviving comments
91
+ -- as is_reply=true. Raw counts are persona-confounded (1.5× vs 0.3×
92
+ -- multipliers) AND conversion-gated (comments are funnel steps;
93
+ -- contributor conversionModifier 1.0 vs reader 0.5), so the raw
94
+ -- comments-per-session DD over-estimates (~2.1) while the
95
+ -- discussion-calibrated DD under-estimates (discussions sit deeper in
96
+ -- their funnel — EL step 4 vs comment step 3 — so the calibrator
97
+ -- over-corrects). The two bracket the 1.5 knob with sign-known biases.
98
+ SELECT u.role, count(DISTINCT u.distinct_id) AS users,
99
+ round(sum(p.comments)::DOUBLE / nullif(sum(p.sessions), 0), 4) AS comments_per_session,
100
+ round(sum(p.discussions)::DOUBLE / nullif(sum(p.sessions), 0), 4) AS discussions_per_session
101
+ FROM users u JOIN per_user p ON p.uid = u.distinct_id::VARCHAR
102
+ WHERE u.role IN ('contributor', 'reader')
103
+ GROUP BY 1 ORDER BY 1;
104
+ -- read: corrected DD = (comments DD ÷ discussions DD) ≤ 1.5 ≤ raw comments DD
105
+
106
+ -- reply-share composition: clones are always replies → contributor share
107
+ -- shifts from the organic 2/3 to (2/3 + 0.5)/1.5 ≈ 0.778 (drop-invariant)
108
+ SELECT u.role,
109
+ round(count(*) FILTER (WHERE e.is_reply = true)::DOUBLE / count(*), 4) AS reply_share
110
+ FROM users u JOIN ev e ON e.uid = u.distinct_id::VARCHAR AND e.event = 'comment posted'
111
+ WHERE u.role IN ('contributor', 'reader')
112
+ GROUP BY 1 ORDER BY 1;
113
+ -- read: contributor ≈ 0.778, reader ≈ 0.667
114
+
115
+
116
+ -- ── H5-edit-war ─────────────────────────────────────────────
117
+ -- >5 edits at hook time → ALL edit_quality redrawn U[1.0, 2.0]. Edits
118
+ -- never deleted → output ≥6 IMPLIES treatment → redraw is EXACT on the
119
+ -- war arm: avg 1.5, zero values above 2.0.
120
+ SELECT CASE WHEN p.edits > 5 THEN 'war' WHEN p.edits BETWEEN 1 AND 4 THEN 'calm' END AS arm,
121
+ count(DISTINCT p.uid) AS users, round(avg(e.edit_quality), 3) AS avg_q,
122
+ count(*) FILTER (WHERE e.edit_quality > 2.0) AS over_cap
123
+ FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'article edited'
124
+ WHERE p.edits > 5 OR p.edits BETWEEN 1 AND 4
125
+ GROUP BY 1 ORDER BY 1;
126
+ -- read: war avg_q ≈ 1.5 with over_cap = 0; calm avg_q ≈ 3 (organic 1-5)
127
+
128
+
129
+ -- ── H6-lurker-churn ─────────────────────────────────────────
130
+ -- <5 events at hook time → 60% of post-day-10 events dropped (keep 0.4).
131
+ -- Output n∈[2,4] implies treated; output n∈[5,8] implies untreated.
132
+ -- n=1 users carry no churn information (post=0 and days-5-10=0 by
133
+ -- construction) and are excluded; the control band is ADJACENT ([5,8],
134
+ -- not [6,10]) because the front-loading calibration transfers better
135
+ -- between closer activity levels. Arms restricted to role 'reader'.
136
+ -- Raw post/pre ratio is confounded by organic front-loading, so calibrate
137
+ -- on the pre-cutoff half-split (days 0-5 vs 5-10 — never touched by H6).
138
+ SELECT CASE WHEN p.n BETWEEN 2 AND 4 THEN 'tiny' WHEN p.n BETWEEN 5 AND 8 THEN 'small' END AS arm,
139
+ count(DISTINCT p.uid) AS users,
140
+ round(sum(CASE WHEN e.t > fe.f + INTERVAL 10 DAY THEN 1 ELSE 0 END)::DOUBLE
141
+ / nullif(sum(CASE WHEN e.t <= fe.f + INTERVAL 10 DAY THEN 1 ELSE 0 END), 0), 4) AS rho,
142
+ round(sum(CASE WHEN e.t > fe.f + INTERVAL 5 DAY AND e.t <= fe.f + INTERVAL 10 DAY THEN 1 ELSE 0 END)::DOUBLE
143
+ / nullif(sum(CASE WHEN e.t <= fe.f + INTERVAL 5 DAY THEN 1 ELSE 0 END), 0), 4) AS rho_pre
144
+ FROM per_user p
145
+ JOIN first_ev fe ON fe.uid = p.uid
146
+ JOIN ev e ON e.uid = p.uid
147
+ JOIN users u ON u.distinct_id::VARCHAR = p.uid AND u.role = 'reader'
148
+ WHERE (p.n BETWEEN 2 AND 4 OR p.n BETWEEN 5 AND 8)
149
+ AND fe.f < TIMESTAMP '2026-04-15' -- exclude births without a post window
150
+ GROUP BY 1 ORDER BY 1;
151
+ -- read: (rho_tiny/rho_small) ÷ (rho_pre_tiny/rho_pre_small) ≈ 0.4 keep rate
152
+
153
+
154
+ -- ── H7-creator-profiles ─────────────────────────────────────
155
+ -- user hook overwrites per role: creator art U[50,200] rep U[80,100];
156
+ -- moderator U[10,50]/U[40,70]; contributor U[1,15]/U[15,50]; reader 0/U[0,20].
157
+ SELECT role, count(*) AS users,
158
+ min(reputation_score) AS min_rep, round(avg(reputation_score), 2) AS avg_rep, max(reputation_score) AS max_rep,
159
+ min(articles_created) AS min_art, max(articles_created) AS max_art
160
+ FROM users GROUP BY 1 ORDER BY 1;
161
+ -- read: ranges exact per role; creator avg_rep ≈ 90
162
+
163
+
164
+ -- ── H8-pro-content-lift ─────────────────────────────────────
165
+ -- free/non-supporter tiers drop 65% of ALL 'comment posted' (keep 0.35).
166
+ -- Tier ⊥ persona → H4's clone factor cancels in the pooled ratio.
167
+ SELECT CASE WHEN e.subscription_tier IN ('pro', 'supporter') THEN 'paid' ELSE 'free' END AS arm,
168
+ count(DISTINCT e.uid) AS users,
169
+ round(count(*) FILTER (WHERE e.event = 'comment posted')::DOUBLE
170
+ / nullif(count(*) FILTER (WHERE e.event = 'app session'), 0), 4) AS comments_per_session
171
+ FROM ev e GROUP BY 1 ORDER BY 1;
172
+ -- read: free/paid comments-per-session ≈ 0.35 (the exact keep rate)
173
+
174
+
175
+ -- ── H9-content-ttc ──────────────────────────────────────────
176
+ -- funnel-post scales Content Creation gaps: pro/supporter ×0.77, free
177
+ -- ×1.25 (v1.6 scopes the hook to Content Creation only).
178
+ -- CAUTION: cross-event TTC SQL here is the documented greedy-single-pass
179
+ -- limitation — it pairs views/publishes across funnel instances and buries
180
+ -- the signal. The story asserts TTC through the Mixpanel-aligned emulator
181
+ -- (timeToConvert, 60h window = 48h generative × 1.25 max stretch); trust
182
+ -- the story verdict, not ad-hoc pair SQL.
183
+ SELECT u.subscription_tier AS tier, count(*) AS published_events
184
+ FROM ev e JOIN users u ON u.distinct_id::VARCHAR = e.uid
185
+ WHERE e.event = 'article published' GROUP BY 1 ORDER BY 1;
186
+
187
+
188
+ -- ── H10-article-magic-number ────────────────────────────────
189
+ -- sweet spot 2-5 publishes → upvote_count ×1.35 (rounded); 6+ publishes →
190
+ -- creator burnout from day 60 (2026-03-02): 40% of upvote events dropped
191
+ -- after the cutoff. Value read is persona-clean (iid draw).
192
+ SELECT CASE WHEN p.pubs BETWEEN 2 AND 5 THEN 'sweet' WHEN p.pubs <= 1 THEN 'low'
193
+ WHEN p.pubs >= 6 THEN 'over' END AS arm,
194
+ count(DISTINCT p.uid) AS users, round(avg(e.upvote_count), 3) AS avg_uc
195
+ FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'upvote given'
196
+ GROUP BY 1 ORDER BY 1;
197
+ -- read: sweet/low avg_uc ≈ 1.35-1.4 (integer rounding drifts it up)
198
+
199
+ -- volume read: publish count is intrinsically coupled to activity, so NO
200
+ -- cross-arm level comparison works (organic upvote share differs 23-58%
201
+ -- across publish bands whatever the denominator). The calendar edge makes
202
+ -- it a difference-in-differences: each arm's own after/before
203
+ -- upvotes-per-session ratio cancels its activity composition (measured
204
+ -- arm-invariant to ~0.1% on untreated data). Contributors only.
205
+ SELECT CASE WHEN p.pubs BETWEEN 2 AND 5 THEN 'sweet' WHEN p.pubs >= 6 THEN 'over' END AS arm,
206
+ CASE WHEN e.t >= TIMESTAMP '2026-03-02' THEN 'after' ELSE 'before' END AS period,
207
+ count(DISTINCT p.uid) AS users,
208
+ count(*) FILTER (WHERE e.event = 'upvote given') AS ups,
209
+ count(*) FILTER (WHERE e.event = 'app session') AS sessions
210
+ FROM per_user p
211
+ JOIN users u ON u.distinct_id::VARCHAR = p.uid AND u.role = 'contributor'
212
+ JOIN ev e ON e.uid = p.uid
213
+ WHERE p.pubs >= 2
214
+ GROUP BY 1, 2 ORDER BY 1, 2;
215
+ -- read: DiD = (over after/before ups-per-session) ÷ (sweet after/before)
216
+ -- ≈ 0.6 (the keep rate)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * community.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./community.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/community/community.js --data-prefix verify-community
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/community/community.js verify-community
12
+ * Run:
13
+ * node dungeons/vertical/community/community.verify.mjs [prefix] # default verify-community
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 './community.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-community';
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/community/community.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`community — 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/H9 emulator stories carry their
55
+ // own explicit conversion window (48h Content Creation generative window ×
56
+ // the 1.25 free-tier stretch = 60h, covering the stretched support), so no
57
+ // funnel-default 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);