@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,173 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- media.sql — human-eyeball inspection queries (v1.6)
|
|
3
|
+
-- ============================================================
|
|
4
|
+
-- Mirrors the story reads in media.js `stories`. Run after:
|
|
5
|
+
-- node scripts/verify-runner.mjs dungeons/vertical/media/media.js verify-media
|
|
6
|
+
--
|
|
7
|
+
-- Derivation notes:
|
|
8
|
+
-- - H1 is a completions-per-selection ratio-of-ratios vs ANIMATION:
|
|
9
|
+
-- animation absorbs the same H6 selection-count inflation as
|
|
10
|
+
-- documentary, and per-selection normalization cancels the
|
|
11
|
+
-- favored-index popularity skew (action/romance selections run hot).
|
|
12
|
+
-- Genre exists on completions only via core-viewing funnel props.
|
|
13
|
+
-- - H7 normalizes rated counts by total events pre/post day 60 —
|
|
14
|
+
-- cancels the user-growth ramp (1/0.7 = 1.43 mechanism).
|
|
15
|
+
-- - H8's clean read is completion_percent; the duration x1.15 is
|
|
16
|
+
-- cancelled by subtitle users skewing into the H9 over-bucket.
|
|
17
|
+
-- - H9 over/sweet = 0.5/1.25 = 0.40 exact — the engagement confound
|
|
18
|
+
-- cancels between the two high-activity buckets.
|
|
19
|
+
-- - H10's TTC leg is NOT visible in cross-event SQL; see the emulator
|
|
20
|
+
-- timeToConvert assertion in media.js stories (6-HOUR window — at
|
|
21
|
+
-- multi-day windows the median sits on a bimodal mode boundary and
|
|
22
|
+
-- flips on sampling noise).
|
|
23
|
+
|
|
24
|
+
-- Hook 1: GENRE FUNNEL CONVERSION — doc/anim comp-per-sel RoR ~0.75
|
|
25
|
+
WITH g AS (
|
|
26
|
+
SELECT genre,
|
|
27
|
+
COUNT(*) FILTER (WHERE event = 'content selected') AS sel_n,
|
|
28
|
+
COUNT(*) FILTER (WHERE event = 'playback completed') AS comp_n
|
|
29
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
30
|
+
WHERE genre IS NOT NULL AND event IN ('content selected', 'playback completed')
|
|
31
|
+
GROUP BY genre
|
|
32
|
+
)
|
|
33
|
+
SELECT genre, sel_n, comp_n, ROUND(comp_n::DOUBLE / sel_n, 4) AS comp_per_sel
|
|
34
|
+
FROM g ORDER BY comp_per_sel;
|
|
35
|
+
|
|
36
|
+
-- Hook 2: BINGE-WATCHING — binge cohort (max completed-streak >= 3,
|
|
37
|
+
-- started ignored) ~1.6x completions per user
|
|
38
|
+
WITH seq AS (
|
|
39
|
+
SELECT user_id::VARCHAR AS uid, event,
|
|
40
|
+
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY time::TIMESTAMP, event) AS rn
|
|
41
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
42
|
+
WHERE event != 'playback started'
|
|
43
|
+
), runs AS (
|
|
44
|
+
SELECT uid, rn - ROW_NUMBER() OVER (PARTITION BY uid ORDER BY rn) AS grp
|
|
45
|
+
FROM seq WHERE event = 'playback completed'
|
|
46
|
+
), streaks AS (
|
|
47
|
+
SELECT uid, MAX(cnt) AS max_streak
|
|
48
|
+
FROM (SELECT uid, grp, COUNT(*) AS cnt FROM runs GROUP BY uid, grp) GROUP BY uid
|
|
49
|
+
), pu AS (
|
|
50
|
+
SELECT user_id::VARCHAR AS uid,
|
|
51
|
+
COUNT(*) FILTER (WHERE event = 'playback completed') AS completions
|
|
52
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
53
|
+
GROUP BY 1
|
|
54
|
+
)
|
|
55
|
+
SELECT COALESCE(s.max_streak >= 3, false) AS binge, COUNT(*) AS users,
|
|
56
|
+
ROUND(AVG(p.completions), 2) AS completions_pu
|
|
57
|
+
FROM pu p LEFT JOIN streaks s USING (uid)
|
|
58
|
+
GROUP BY (COALESCE(s.max_streak >= 3, false)) ORDER BY binge;
|
|
59
|
+
|
|
60
|
+
-- Hook 3: WEEKEND VS WEEKDAY — ~1.45x avg duration (1.5 engineered,
|
|
61
|
+
-- diluted by fresh-draw clone durations)
|
|
62
|
+
SELECT
|
|
63
|
+
CASE WHEN dayofweek(time::TIMESTAMP) IN (0, 6) THEN 'weekend' ELSE 'weekday' END AS bucket,
|
|
64
|
+
COUNT(*) AS n, ROUND(AVG(watch_duration_min), 1) AS avg_min
|
|
65
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
66
|
+
WHERE event = 'playback completed' AND watch_duration_min IS NOT NULL
|
|
67
|
+
GROUP BY (CASE WHEN dayofweek(time::TIMESTAMP) IN (0, 6) THEN 'weekend' ELSE 'weekday' END);
|
|
68
|
+
|
|
69
|
+
-- Hook 4: AD FATIGUE CHURN — fatigued post/pre ~0.04x rest (among users
|
|
70
|
+
-- whose lifecycle spans past day 52; late-born users dilute otherwise)
|
|
71
|
+
WITH pu AS (
|
|
72
|
+
SELECT user_id::VARCHAR AS uid, MIN(time::TIMESTAMP) AS t0, MAX(time::TIMESTAMP) AS tlast
|
|
73
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
74
|
+
GROUP BY 1
|
|
75
|
+
), flags AS (
|
|
76
|
+
SELECT p.uid,
|
|
77
|
+
COUNT(*) FILTER (WHERE e.event = 'ad impression' AND e.time::TIMESTAMP < p.t0 + INTERVAL 45 DAY) AS early_ads,
|
|
78
|
+
COUNT(*) FILTER (WHERE e.time::TIMESTAMP > p.t0 + INTERVAL 45 DAY) AS post,
|
|
79
|
+
COUNT(*) FILTER (WHERE e.time::TIMESTAMP <= p.t0 + INTERVAL 45 DAY) AS pre
|
|
80
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
81
|
+
JOIN pu p ON e.user_id::VARCHAR = p.uid
|
|
82
|
+
WHERE p.tlast > p.t0 + INTERVAL 52 DAY
|
|
83
|
+
GROUP BY 1
|
|
84
|
+
)
|
|
85
|
+
SELECT (early_ads >= 5) AS fatigued, COUNT(*) AS users,
|
|
86
|
+
ROUND(AVG(post::DOUBLE / NULLIF(pre, 0)), 4) AS avg_post_pre
|
|
87
|
+
FROM flags WHERE pre > 0
|
|
88
|
+
GROUP BY (early_ads >= 5) ORDER BY fatigued;
|
|
89
|
+
|
|
90
|
+
-- Hook 5: NEW RELEASE SPIKE — 20% blockbuster share of selections in
|
|
91
|
+
-- days 50-64, zero outside; blockbuster ratings pinned 4-5
|
|
92
|
+
SELECT (day_idx BETWEEN 50 AND 64) AS in_window, COUNT(*) AS n,
|
|
93
|
+
ROUND(AVG((content_id LIKE 'blockbuster%')::INT), 4) AS bb_share
|
|
94
|
+
FROM (
|
|
95
|
+
SELECT content_id,
|
|
96
|
+
date_diff('day', TIMESTAMP '2026-01-01 00:00:00', time::TIMESTAMP) AS day_idx
|
|
97
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
98
|
+
WHERE event = 'content selected'
|
|
99
|
+
)
|
|
100
|
+
GROUP BY (day_idx BETWEEN 50 AND 64) ORDER BY in_window;
|
|
101
|
+
|
|
102
|
+
SELECT COUNT(*) AS bb_rated, MIN(rating) AS min_rating, MAX(rating) AS max_rating
|
|
103
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
104
|
+
WHERE event = 'content rated' AND content_id LIKE 'blockbuster%';
|
|
105
|
+
|
|
106
|
+
-- Hook 6: KIDS PROFILE SAFETY — anim+doc ~32% of selected (vs ~20% base).
|
|
107
|
+
-- Read content selected ONLY: playback started carries genre solely via
|
|
108
|
+
-- funnel-2 props or this hook's stamp (its genre subset reads ~48%).
|
|
109
|
+
SELECT COUNT(*) AS n,
|
|
110
|
+
ROUND(AVG((genre IN ('animation', 'documentary'))::INT), 4) AS kid_share
|
|
111
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
112
|
+
WHERE event = 'content selected' AND genre IS NOT NULL;
|
|
113
|
+
|
|
114
|
+
-- Hook 7: REC ENGINE IMPROVEMENT — post/pre rated-share-of-events ~1.43x
|
|
115
|
+
WITH ev AS (
|
|
116
|
+
SELECT event,
|
|
117
|
+
date_diff('day', TIMESTAMP '2026-01-01 00:00:00', time::TIMESTAMP) < 60 AS pre
|
|
118
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
119
|
+
)
|
|
120
|
+
SELECT
|
|
121
|
+
COUNT(*) FILTER (WHERE pre AND event = 'content rated') AS pre_rated,
|
|
122
|
+
COUNT(*) FILTER (WHERE NOT pre AND event = 'content rated') AS post_rated,
|
|
123
|
+
ROUND((COUNT(*) FILTER (WHERE NOT pre AND event = 'content rated')::DOUBLE / COUNT(*) FILTER (WHERE NOT pre))
|
|
124
|
+
/ (COUNT(*) FILTER (WHERE pre AND event = 'content rated')::DOUBLE / COUNT(*) FILTER (WHERE pre)), 4) AS share_ratio
|
|
125
|
+
FROM ev;
|
|
126
|
+
|
|
127
|
+
-- Hook 8: SUBTITLE USERS — completion_percent ~1.28x (the clean read)
|
|
128
|
+
WITH pu AS (
|
|
129
|
+
SELECT user_id::VARCHAR AS uid,
|
|
130
|
+
BOOL_OR(event = 'subtitle toggled' AND action = 'enabled') AS subs
|
|
131
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
132
|
+
GROUP BY 1
|
|
133
|
+
)
|
|
134
|
+
SELECT p.subs, COUNT(DISTINCT p.uid) AS users,
|
|
135
|
+
ROUND(AVG(e.completion_percent), 2) AS avg_cp
|
|
136
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
137
|
+
JOIN pu p ON e.user_id::VARCHAR = p.uid
|
|
138
|
+
WHERE e.event = 'playback completed' AND e.completion_percent IS NOT NULL
|
|
139
|
+
GROUP BY p.subs ORDER BY p.subs;
|
|
140
|
+
|
|
141
|
+
-- Hook 9: REC-CLICK MAGIC NUMBER — sweet/low ~1.25x, over/sweet ~0.40x
|
|
142
|
+
WITH pu AS (
|
|
143
|
+
SELECT user_id::VARCHAR AS uid,
|
|
144
|
+
COUNT(*) FILTER (WHERE event = 'recommendation clicked') AS rcs
|
|
145
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
146
|
+
GROUP BY 1
|
|
147
|
+
)
|
|
148
|
+
SELECT
|
|
149
|
+
CASE WHEN p.rcs BETWEEN 4 AND 6 THEN 'sweet' WHEN p.rcs >= 7 THEN 'over' ELSE 'low' END AS bucket,
|
|
150
|
+
COUNT(DISTINCT p.uid) AS users, ROUND(AVG(e.watch_duration_min), 1) AS avg_dur
|
|
151
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
152
|
+
JOIN pu p ON e.user_id::VARCHAR = p.uid
|
|
153
|
+
WHERE e.event = 'playback completed' AND e.watch_duration_min IS NOT NULL
|
|
154
|
+
GROUP BY (CASE WHEN p.rcs BETWEEN 4 AND 6 THEN 'sweet' WHEN p.rcs >= 7 THEN 'over' ELSE 'low' END)
|
|
155
|
+
ORDER BY bucket;
|
|
156
|
+
|
|
157
|
+
-- Hook 10: CORE VIEWING LOOP — free/premium avg duration ~2.0x. The TTC
|
|
158
|
+
-- leg is not visible here: scaleFunnelTTC rewrites gaps within one funnel
|
|
159
|
+
-- instance per user; see the emulator timeToConvert assertion in media.js
|
|
160
|
+
-- stories (6-hour conversion window).
|
|
161
|
+
SELECT subscription_plan, COUNT(*) AS n, ROUND(AVG(watch_duration_min), 1) AS avg_dur
|
|
162
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true)
|
|
163
|
+
WHERE event = 'playback completed' AND watch_duration_min IS NOT NULL AND subscription_plan IS NOT NULL
|
|
164
|
+
GROUP BY subscription_plan ORDER BY avg_dur;
|
|
165
|
+
|
|
166
|
+
-- Identity invariants: uid coverage 1.0, device coverage ~0.998,
|
|
167
|
+
-- devices/user ~2 (avgDevicePerUser: 2), plan stamped on all events
|
|
168
|
+
SELECT
|
|
169
|
+
ROUND(AVG((user_id IS NOT NULL)::INT), 4) AS uid_share,
|
|
170
|
+
ROUND(AVG((device_id IS NOT NULL)::INT), 4) AS device_share,
|
|
171
|
+
ROUND(AVG((subscription_plan IS NOT NULL)::INT), 4) AS plan_share,
|
|
172
|
+
ROUND(COUNT(DISTINCT device_id)::DOUBLE / COUNT(DISTINCT user_id), 2) AS devices_per_user
|
|
173
|
+
FROM read_json_auto('data/verify-media-EVENTS*.json', sample_size=-1, union_by_name=true);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* media.verify.mjs — thin wrapper around the story runner.
|
|
4
|
+
*
|
|
5
|
+
* All verification logic lives in the `stories` export of
|
|
6
|
+
* ./media.js; this script just streams the shards and
|
|
7
|
+
* delegates. It is equivalent to:
|
|
8
|
+
*
|
|
9
|
+
* node scripts/verify-stories.mjs dungeons/vertical/media/media.js --data-prefix verify-media
|
|
10
|
+
*
|
|
11
|
+
* Generate first:
|
|
12
|
+
* node scripts/verify-runner.mjs dungeons/vertical/media/media.js verify-media
|
|
13
|
+
* Run:
|
|
14
|
+
* node dungeons/vertical/media/media.verify.mjs [prefix] # default verify-media
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import readline from 'node:readline';
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
20
|
+
import { promisify } from 'node:util';
|
|
21
|
+
import { buildIdentityMap, evaluateStories, VERDICT_RANK } from '@ak--47/dungeon-master/verify';
|
|
22
|
+
import config, { stories } from './media.js';
|
|
23
|
+
|
|
24
|
+
const PREFIX = process.argv[2] || 'verify-media';
|
|
25
|
+
const prefixPath = path.join('data', PREFIX);
|
|
26
|
+
|
|
27
|
+
async function loadShards(suffix) {
|
|
28
|
+
// streaming load: full-fidelity EVENTS shards exceed the readFileSync cap
|
|
29
|
+
const dir = path.dirname(prefixPath), base = path.basename(prefixPath);
|
|
30
|
+
const out = [];
|
|
31
|
+
if (!fs.existsSync(dir)) return out;
|
|
32
|
+
for (const f of fs.readdirSync(dir).filter(f => f.startsWith(`${base}-${suffix}`) && f.endsWith('.json')).sort()) {
|
|
33
|
+
const rl = readline.createInterface({ input: fs.createReadStream(path.join(dir, f)), crlfDelay: Infinity });
|
|
34
|
+
for await (const line of rl) {
|
|
35
|
+
if (line.trim()) out.push(JSON.parse(line));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const events = await loadShards('EVENTS');
|
|
42
|
+
const profiles = await loadShards('USERS');
|
|
43
|
+
if (!events.length) {
|
|
44
|
+
console.error(`no shards at ${prefixPath}-EVENTS*.json — run: node scripts/verify-runner.mjs dungeons/vertical/media/media.js ${PREFIX}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(`media — events=${events.length} users=${profiles.length} (${prefixPath})`);
|
|
48
|
+
|
|
49
|
+
const execFileP = promisify(execFile);
|
|
50
|
+
const runSql = async (sql) => {
|
|
51
|
+
const { stdout } = await execFileP('duckdb', ['-json', '-c', sql.replaceAll('{{PREFIX}}', prefixPath)], { maxBuffer: 64 * 1024 * 1024 });
|
|
52
|
+
return stdout.trim() ? JSON.parse(stdout) : [];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Nine stories are DuckDB-backed (behavioral cohorts + ratio-of-ratios reads
|
|
56
|
+
// that cancel popularity/H6/growth confounds); H10 also dispatches to
|
|
57
|
+
// emulateBreakdown's timeToConvert at a 6-hour window — at multi-day
|
|
58
|
+
// windows the median TTC sits on a bimodal mode boundary (within-session
|
|
59
|
+
// vs cross-session) and flips on sampling noise.
|
|
60
|
+
const results = await evaluateStories(stories, events, {
|
|
61
|
+
profiles,
|
|
62
|
+
funnels: config.funnels,
|
|
63
|
+
identityMap: buildIdentityMap(profiles),
|
|
64
|
+
runSql,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let worst = 'NAILED';
|
|
68
|
+
for (const r of results) {
|
|
69
|
+
console.log(`${r.verdict.padEnd(7)} ${r.id}`);
|
|
70
|
+
for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
|
|
71
|
+
if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
|
|
72
|
+
}
|
|
73
|
+
console.log(`\nworst verdict: ${worst}`);
|
|
74
|
+
process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);
|