@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.
Files changed (149) 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/powertools/SKILL.md +75 -0
  6. package/.claude/skills/powertools/pt.mjs +91 -0
  7. package/.claude/skills/powertools/snapshot-project.mjs +124 -0
  8. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  9. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  10. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  11. package/.claude/skills/write-hooks/SKILL.md +156 -18
  12. package/CHANGELOG.md +224 -0
  13. package/HOOKS.md +566 -112
  14. package/README.md +30 -9
  15. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  16. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  17. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  18. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  19. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  20. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  21. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  22. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  23. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  24. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  25. package/dungeons/technical/stories-verify.js +152 -0
  26. package/dungeons/vertical/README.md +154 -0
  27. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  28. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  29. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  30. package/dungeons/vertical/community/community.js +1589 -0
  31. package/dungeons/vertical/community/community.sql +216 -0
  32. package/dungeons/vertical/community/community.verify.mjs +72 -0
  33. package/dungeons/vertical/crypto/crypto.js +1483 -0
  34. package/dungeons/vertical/crypto/crypto.sql +194 -0
  35. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  36. package/dungeons/vertical/dating/dating.js +1540 -0
  37. package/dungeons/vertical/dating/dating.sql +288 -0
  38. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  39. package/dungeons/vertical/devtools/devtools.js +1524 -0
  40. package/dungeons/vertical/devtools/devtools.sql +161 -0
  41. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  42. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  43. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  44. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  45. package/dungeons/vertical/education/education.js +1763 -0
  46. package/dungeons/vertical/education/education.sql +243 -0
  47. package/dungeons/vertical/education/education.verify.mjs +75 -0
  48. package/dungeons/vertical/fintech/fintech.js +1410 -0
  49. package/dungeons/vertical/fintech/fintech.sql +175 -0
  50. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  51. package/dungeons/vertical/fitness/fitness.js +1495 -0
  52. package/dungeons/vertical/fitness/fitness.sql +206 -0
  53. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  54. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  55. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  56. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  57. package/dungeons/vertical/gaming/gaming.js +1741 -0
  58. package/dungeons/vertical/gaming/gaming.sql +249 -0
  59. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  60. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  61. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  62. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  63. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  64. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  65. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  66. package/dungeons/vertical/logistics/logistics.js +1541 -0
  67. package/dungeons/vertical/logistics/logistics.sql +205 -0
  68. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  69. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  70. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  71. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  72. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  73. package/dungeons/vertical/media/media.sql +173 -0
  74. package/dungeons/vertical/media/media.verify.mjs +74 -0
  75. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  76. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  77. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  78. package/dungeons/vertical/sass/sass.js +1733 -0
  79. package/dungeons/vertical/sass/sass.sql +173 -0
  80. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  81. package/dungeons/vertical/social/social.js +1456 -0
  82. package/dungeons/vertical/social/social.sql +160 -0
  83. package/dungeons/vertical/social/social.verify.mjs +76 -0
  84. package/dungeons/vertical/streaming/streaming.js +677 -0
  85. package/dungeons/vertical/streaming/streaming.sql +101 -0
  86. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  87. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  88. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  89. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  90. package/dungeons/vertical/travel/travel.js +1603 -0
  91. package/dungeons/vertical/travel/travel.sql +236 -0
  92. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  93. package/index.js +7 -3
  94. package/lib/core/config-validator.js +37 -2
  95. package/lib/core/context.js +4 -2
  96. package/lib/core/dungeon-loader.js +20 -15
  97. package/lib/core/dungeon-to-json.js +22 -4
  98. package/lib/core/storage.js +3 -3
  99. package/lib/generators/funnels.js +36 -9
  100. package/lib/hook-helpers/cohort.js +53 -0
  101. package/lib/hook-helpers/index.js +8 -0
  102. package/lib/hook-helpers/mutate.js +1 -1
  103. package/lib/hook-helpers/shape.js +319 -0
  104. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  105. package/lib/hook-patterns/attributed-by-source.js +60 -58
  106. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  107. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  108. package/lib/hook-patterns/index.js +1 -1
  109. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  110. package/lib/orchestrators/user-loop.js +41 -0
  111. package/lib/templates/story-spec.schema.json +175 -0
  112. package/lib/utils/json-evaluator.js +1 -1
  113. package/lib/utils/utils.js +151 -59
  114. package/lib/verify/coerce.js +186 -0
  115. package/lib/verify/counting.js +270 -32
  116. package/lib/verify/emulate-breakdown.js +1285 -203
  117. package/lib/verify/first-time.js +89 -0
  118. package/lib/verify/flows.js +679 -0
  119. package/lib/verify/formula.js +259 -0
  120. package/lib/verify/funnel-engine.js +697 -113
  121. package/lib/verify/index.js +32 -4
  122. package/lib/verify/sessionize.js +275 -0
  123. package/lib/verify/story-runner.js +483 -0
  124. package/lib/verify/verify-dungeon.js +59 -37
  125. package/package.json +2 -1
  126. package/scripts/extract-dungeon-schema.mjs +2 -2
  127. package/scripts/run-many.mjs +2 -2
  128. package/scripts/verify-runner.mjs +2 -2
  129. package/scripts/verify-stories.mjs +244 -0
  130. package/types.d.ts +298 -30
  131. package/dungeons/vertical/ai-platform.js +0 -818
  132. package/dungeons/vertical/community.js +0 -966
  133. package/dungeons/vertical/crypto.js +0 -895
  134. package/dungeons/vertical/dating.js +0 -830
  135. package/dungeons/vertical/devtools.js +0 -1050
  136. package/dungeons/vertical/ecommerce.js +0 -811
  137. package/dungeons/vertical/education.js +0 -964
  138. package/dungeons/vertical/fintech.js +0 -946
  139. package/dungeons/vertical/fitness.js +0 -935
  140. package/dungeons/vertical/food-delivery.js +0 -934
  141. package/dungeons/vertical/gaming.js +0 -1120
  142. package/dungeons/vertical/healthcare.js +0 -945
  143. package/dungeons/vertical/insurance-application.js +0 -1039
  144. package/dungeons/vertical/logistics.js +0 -986
  145. package/dungeons/vertical/marketplace.js +0 -1019
  146. package/dungeons/vertical/real-estate.js +0 -872
  147. package/dungeons/vertical/sass.js +0 -995
  148. package/dungeons/vertical/social.js +0 -904
  149. package/dungeons/vertical/travel.js +0 -930
@@ -0,0 +1,160 @@
1
+ -- ============================================================
2
+ -- social.js — hook inspection queries (v1.6.0 stories rebuild)
3
+ --
4
+ -- The verification CONTRACT lives in the `stories` export of
5
+ -- social.js (run via social.verify.mjs). These queries are for
6
+ -- manual inspection of the same reads.
7
+ --
8
+ -- Derivation notes (measured at 2K reduced scale + organic
9
+ -- counterfactual run — same seed, identity hook):
10
+ -- - H1's old "10+ post created" cohort read is dead: 85% of
11
+ -- users clear 10 FINAL posts (clones inflate counts). The
12
+ -- live read is concentration: top-3% engagement share 0.62
13
+ -- vs organic 0.06.
14
+ -- - H2's 1.5x posts ratio decomposes into 1.24x activity
15
+ -- confound x 1.21x true hook increment.
16
+ -- - Organic DOW is FLAT (soup weights >= 1.0 accept-always);
17
+ -- H8's 1.25x weekend ratio is pure hook. The v1.5 "soup
18
+ -- baseline ~0.55" claim was wrong.
19
+ -- - H9 must be read by FINAL post counts (the analyst view) —
20
+ -- the hook buckets on pre-injection counts, so the final
21
+ -- 3-7 bucket is dominated by boosted 0-2 dodgers: sweet
22
+ -- avg ~250, low ~200, over ~145.
23
+ -- - H10's funnel-post gap scaling is NOT visible to
24
+ -- MIN(step)-join SQL; use the emulator via social.verify.mjs.
25
+ -- The MIN-join below shows direction only.
26
+ --
27
+ -- Replace data/verify-social with your run prefix.
28
+ -- ============================================================
29
+
30
+ -- H1: VIRAL CONTENT — top-3% of users' share of engagement events
31
+ -- Expected: ~0.62 (organic counterfactual 0.057)
32
+ WITH pu AS (
33
+ SELECT user_id::VARCHAR AS uid,
34
+ COUNT(*) FILTER (WHERE event IN ('post viewed', 'post liked', 'post shared')) AS eng
35
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
36
+ WHERE user_id IS NOT NULL GROUP BY 1
37
+ ), ranked AS (
38
+ SELECT eng, ROW_NUMBER() OVER (ORDER BY eng DESC) AS rn,
39
+ COUNT(*) OVER () AS n, SUM(eng) OVER () AS tot
40
+ FROM pu
41
+ )
42
+ SELECT ROUND(SUM(eng)::DOUBLE / MAX(tot), 4) AS top3_share, MAX(n) AS users
43
+ FROM ranked WHERE rn <= CEIL(n * 0.03);
44
+
45
+ -- H2: FOLLOW-BACK SNOWBALL — 5+ user-followed → more posts
46
+ -- Expected: big/small posts-per-user ~1.50 (42.3 vs 28.2 at 2K)
47
+ WITH pu AS (
48
+ SELECT user_id::VARCHAR AS uid,
49
+ COUNT(*) FILTER (WHERE event = 'user followed') AS fc,
50
+ COUNT(*) FILTER (WHERE event = 'post created') AS pc
51
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
52
+ WHERE user_id IS NOT NULL GROUP BY 1
53
+ )
54
+ SELECT (fc >= 5) AS big_followers, COUNT(*) AS users, ROUND(AVG(pc), 2) AS avg_posts
55
+ FROM pu GROUP BY 1 ORDER BY 1;
56
+
57
+ -- H3 + H5: SOURCE MIX by window (algorithm change @d45, notifications @d30)
58
+ -- Expected: pre_d30 feed ~0.78, notif ~0.03; d30_45 feed ~0.54, notif ~0.32;
59
+ -- post_d45 explore ~0.54, notif ~0.32, feed ~0.05
60
+ SELECT
61
+ CASE WHEN time::TIMESTAMP < TIMESTAMP '2026-01-31 00:00:00' THEN 'pre_d30'
62
+ WHEN time::TIMESTAMP < TIMESTAMP '2026-02-15 00:00:00' THEN 'd30_45'
63
+ ELSE 'post_d45' END AS win,
64
+ COUNT(*) AS n,
65
+ ROUND(AVG((source = 'feed')::INT), 4) AS feed,
66
+ ROUND(AVG((source = 'explore')::INT), 4) AS explore,
67
+ ROUND(AVG((source = 'notification')::INT), 4) AS notif,
68
+ ROUND(AVG((source = 'profile')::INT), 4) AS prof
69
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
70
+ WHERE event = 'post viewed'
71
+ GROUP BY 1 ORDER BY 1;
72
+
73
+ -- H4: ENGAGEMENT BAIT — share of post-viewed events with crushed (<=5s) duration
74
+ -- Expected: ~0.20 (organic 0.000 — generator floors durations above 5s)
75
+ SELECT COUNT(*) AS views,
76
+ ROUND(AVG((view_duration_sec <= 5)::INT), 4) AS crushed_share,
77
+ ROUND(AVG(view_duration_sec) FILTER (WHERE view_duration_sec <= 5), 2) AS avg_crushed_dur
78
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
79
+ WHERE event = 'post viewed' AND view_duration_sec IS NOT NULL;
80
+
81
+ -- H6: CREATOR MONETIZATION — subscribers (~78% of users) vs non
82
+ -- Expected: posts/user ratio ~3.32; stories/user ~3.82 (purer read —
83
+ -- H2 dupes lift non-sub posts but never stories)
84
+ WITH pu AS (
85
+ SELECT user_id::VARCHAR AS uid,
86
+ BOOL_OR(event = 'creator subscription started') AS sub,
87
+ COUNT(*) FILTER (WHERE event = 'post created') AS pc,
88
+ COUNT(*) FILTER (WHERE event = 'story created') AS sc
89
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
90
+ WHERE user_id IS NOT NULL GROUP BY 1
91
+ )
92
+ SELECT sub, COUNT(*) AS users, ROUND(AVG(pc), 2) AS avg_posts, ROUND(AVG(sc), 2) AS avg_stories
93
+ FROM pu GROUP BY 1 ORDER BY 1;
94
+
95
+ -- H7: TOXICITY CHURN — per-user post/pre-d30 event ratio, reporters vs normal
96
+ -- Expected: reporters ~1.27 vs normal ~3.21 → contrast ~0.40 (60% drop knob;
97
+ -- growth shape makes the raw post/pre ~3.2 for everyone)
98
+ WITH pu AS (
99
+ SELECT user_id::VARCHAR AS uid,
100
+ COUNT(*) FILTER (WHERE event = 'report submitted') AS rep,
101
+ COUNT(*) FILTER (WHERE time::TIMESTAMP <= TIMESTAMP '2026-01-31 00:00:00') AS pre,
102
+ COUNT(*) FILTER (WHERE time::TIMESTAMP > TIMESTAMP '2026-01-31 00:00:00') AS post
103
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
104
+ WHERE user_id IS NOT NULL GROUP BY 1
105
+ )
106
+ SELECT (rep >= 2) AS reporter, COUNT(*) AS users, ROUND(AVG(post::DOUBLE / pre), 3) AS post_pre_ratio
107
+ FROM pu WHERE pre > 0 GROUP BY 1 ORDER BY 1;
108
+
109
+ -- H8: WEEKEND SURGE — weekend vs weekday daily creation rate
110
+ -- Expected: ~1.25x (organic counterfactual 0.98 — flat DOW baseline)
111
+ WITH d AS (
112
+ SELECT time::DATE AS dt, COUNT(*) AS n
113
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
114
+ WHERE event IN ('post created', 'story created') GROUP BY 1
115
+ )
116
+ SELECT
117
+ ROUND(AVG(n) FILTER (WHERE EXTRACT(DOW FROM dt) IN (0, 6)), 1) AS weekend_per_day,
118
+ ROUND(AVG(n) FILTER (WHERE EXTRACT(DOW FROM dt) NOT IN (0, 6)), 1) AS weekday_per_day,
119
+ ROUND(AVG(n) FILTER (WHERE EXTRACT(DOW FROM dt) IN (0, 6))
120
+ / AVG(n) FILTER (WHERE EXTRACT(DOW FROM dt) NOT IN (0, 6)), 3) AS ratio
121
+ FROM d;
122
+
123
+ -- H9: MAGIC NUMBER — comment_length by FINAL post-count bucket
124
+ -- Expected: sweet(3-7) ~250, low(0-2) ~200, over(8+) ~145; sweet/over ~1.72
125
+ WITH pu AS (
126
+ SELECT user_id::VARCHAR AS uid,
127
+ COUNT(*) FILTER (WHERE event = 'post created') AS pc
128
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
129
+ WHERE user_id IS NOT NULL GROUP BY 1
130
+ )
131
+ SELECT CASE WHEN pu.pc >= 8 THEN 'over' WHEN pu.pc >= 3 THEN 'sweet' ELSE 'low' END AS bucket,
132
+ COUNT(DISTINCT pu.uid) AS users, COUNT(*) AS n_comments, ROUND(AVG(e.comment_length), 1) AS avg_len
133
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true) e
134
+ JOIN pu ON e.user_id::VARCHAR = pu.uid
135
+ WHERE e.event = 'comment posted' AND e.comment_length IS NOT NULL
136
+ GROUP BY 1 ORDER BY 1;
137
+
138
+ -- H10: ONBOARDING TTC by account_type — DIRECTION ONLY via MIN-join
139
+ -- (funnel-post gap scaling needs per-instance greedy matching; the story
140
+ -- asserts fast/personal ~0.71 through emulateBreakdown @6h window)
141
+ SELECT u.account_type, COUNT(*) AS converters,
142
+ ROUND(MEDIAN(EXTRACT(EPOCH FROM (pc.t - ac.t)) / 60), 1) AS median_ttc_min
143
+ FROM
144
+ (SELECT user_id, MIN(time::TIMESTAMP) AS t
145
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
146
+ WHERE event = 'account created' GROUP BY user_id) ac
147
+ JOIN
148
+ (SELECT user_id, MIN(time::TIMESTAMP) AS t
149
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true)
150
+ WHERE event = 'post created' GROUP BY user_id) pc USING (user_id)
151
+ JOIN read_json_auto('data/verify-social-USERS*.json', sample_size=-1, union_by_name=true) u
152
+ ON ac.user_id = u.distinct_id
153
+ WHERE pc.t > ac.t
154
+ GROUP BY u.account_type ORDER BY u.account_type;
155
+
156
+ -- Identity invariants — expected uid_share 1.0, device_share >= 0.99, devices/user ~2
157
+ SELECT ROUND(AVG((user_id IS NOT NULL)::INT), 4) AS uid_share,
158
+ ROUND(AVG((device_id IS NOT NULL)::INT), 4) AS device_share,
159
+ ROUND(COUNT(DISTINCT device_id)::DOUBLE / COUNT(DISTINCT user_id), 2) AS devices_per_user
160
+ FROM read_json_auto('data/verify-social-EVENTS*.json', sample_size=-1, union_by_name=true);
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * social.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./social.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/social/social.js --data-prefix verify-social
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/social/social.js verify-social
12
+ * Run:
13
+ * node dungeons/vertical/social/social.verify.mjs [prefix] # default verify-social
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 './social.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-social';
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/social/social.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`social — 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
+ // Nine stories are DuckDB-backed (concentration shares, behavioral cohorts,
55
+ // temporal source-mix windows, per-user post/pre retention ratios, DOW daily
56
+ // rates, and final-count frequency buckets). H10 dispatches to
57
+ // emulateBreakdown's timeToConvert at a 6-HOUR window — funnel-post gap
58
+ // scaling is invisible to cross-event SQL, and the onboarding funnel's
59
+ // unique first step (`account created`, isFirstEvent) anchors the greedy
60
+ // evaluator to the exact instance the hook touched, so the read is stable
61
+ // across 1h-24h windows.
62
+ const results = await evaluateStories(stories, events, {
63
+ profiles,
64
+ funnels: config.funnels,
65
+ identityMap: buildIdentityMap(profiles),
66
+ runSql,
67
+ });
68
+
69
+ let worst = 'NAILED';
70
+ for (const r of results) {
71
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
72
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
73
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
74
+ }
75
+ console.log(`\nworst verdict: ${worst}`);
76
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);