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