@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,148 @@
1
+ -- ============================================================
2
+ -- marketplace.sql — human-eyeball inspection queries (v1.6)
3
+ -- ============================================================
4
+ -- Mirrors the story reads in marketplace.js `stories`. Run after:
5
+ -- node scripts/verify-runner.mjs dungeons/vertical/marketplace/marketplace.js verify-marketplace
6
+ --
7
+ -- Derivation notes:
8
+ -- - H1 uses the hook's exact cutoff (time > 2026-02-15T00:00:00Z), not
9
+ -- date_diff day bucketing — day-45 events straddle the boundary.
10
+ -- - H3/H8 read purchases-per-add-to-cart ratio-of-ratios: normalizing by
11
+ -- carts cancels the persona activity gap (power 5.0x vs casual 1.5x
12
+ -- eventMultiplier); the residual is the engineered clone lift (H3
13
+ -- x1.65) or retention edge (H8 4/3).
14
+ -- - H4's clean read is per-user purchases: raw category counts carry the
15
+ -- engine's seeded favored-index popularity skew.
16
+ -- - H5's fast/slow split replicates the hook's charCode hash:
17
+ -- (ascii(first char) + ascii(last char)) % 5 < 2.
18
+ -- - H9 is NOT visible in cross-event SQL — funnel-post scales gaps within
19
+ -- one Browse-to-Purchase instance per user, invisible to MIN→MIN
20
+ -- queries over the full event history. See the emulator timeToConvert
21
+ -- assertion in marketplace.js stories (48-HOUR window, matching the
22
+ -- funnel's timeToConvert; longer windows admit cross-instance chains).
23
+ -- - H10's cohort is total message-sent count — exactly reproducible from
24
+ -- output because H10 runs last and nothing after it drops messages.
25
+
26
+ -- Hook 1: FEE CHANGE IMPACT — avg listing_fee steps ~x1.26 after day 45
27
+ -- (x1.3 knob, Math.floor shaves ~4%)
28
+ SELECT (time::TIMESTAMP > TIMESTAMP '2026-02-15 00:00:00') AS post_d45,
29
+ COUNT(*) AS n, ROUND(AVG(listing_fee), 2) AS avg_fee
30
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
31
+ WHERE event = 'listing created' AND listing_fee IS NOT NULL
32
+ GROUP BY 1 ORDER BY post_d45;
33
+
34
+ -- Hook 2: WEEKEND SHOPPING SURGE — Sat/Sun avg total_amount ~1.21x weekday
35
+ SELECT
36
+ CASE WHEN dayofweek(time::TIMESTAMP) IN (0, 6) THEN 'weekend' ELSE 'weekday' END AS bucket,
37
+ COUNT(*) AS n, ROUND(AVG(total_amount), 1) AS avg_amount
38
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
39
+ WHERE event = 'purchase completed' AND total_amount IS NOT NULL
40
+ GROUP BY (CASE WHEN dayofweek(time::TIMESTAMP) IN (0, 6) THEN 'weekend' ELSE 'weekday' END);
41
+
42
+ -- Hook 3 + Hook 8: purchases-per-cart by segment. Power/casual RoR ~1.67
43
+ -- (H3 x1.65 clones); frequent/casual RoR ~1.33 (H8 4/3 retention edge).
44
+ -- Raw purchases/user power/casual ~6x = clone lift x activity gap.
45
+ WITH pu AS (
46
+ SELECT e.user_id::VARCHAR AS uid, ANY_VALUE(u.segment) AS segment,
47
+ COUNT(*) FILTER (WHERE e.event = 'purchase completed') AS purch,
48
+ COUNT(*) FILTER (WHERE e.event = 'add to cart') AS carts
49
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true) e
50
+ JOIN read_json_auto('data/verify-marketplace-USERS*.json', sample_size=-1, union_by_name=true) u
51
+ ON e.user_id::VARCHAR = u.distinct_id::VARCHAR
52
+ GROUP BY 1
53
+ )
54
+ SELECT segment, COUNT(*) AS users,
55
+ ROUND(AVG(purch), 3) AS purch_pu,
56
+ ROUND(SUM(purch)::DOUBLE / NULLIF(SUM(carts), 0), 4) AS purch_per_cart
57
+ FROM pu GROUP BY segment ORDER BY purch_per_cart DESC;
58
+
59
+ -- Hook 4: ELECTRONICS CATEGORY LIFT — elec-searchers ~1.22x purchases/user
60
+ WITH pu AS (
61
+ SELECT user_id::VARCHAR AS uid,
62
+ BOOL_OR(event = 'item searched' AND category = 'electronics') AS elec,
63
+ COUNT(*) FILTER (WHERE event = 'purchase completed') AS purch
64
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
65
+ GROUP BY 1
66
+ )
67
+ SELECT elec, COUNT(*) AS users, ROUND(AVG(purch), 3) AS purch_pu
68
+ FROM pu GROUP BY elec ORDER BY elec;
69
+
70
+ -- Hook 5: RESPONSE TIME → CONVERSION — fast hash cohort ~10x accepts/user;
71
+ -- avg response_time_hours ~2.25h vs ~22h on message sent
72
+ WITH pu AS (
73
+ SELECT user_id::VARCHAR AS uid,
74
+ (ascii(substr(user_id::VARCHAR, 1, 1)) + ascii(substr(user_id::VARCHAR, -1, 1))) % 5 < 2 AS is_fast,
75
+ BOOL_OR(event = 'message sent') AS has_msg,
76
+ COUNT(*) FILTER (WHERE event = 'offer accepted') AS accepts
77
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
78
+ GROUP BY 1
79
+ )
80
+ SELECT is_fast, COUNT(*) AS users, ROUND(AVG(accepts), 3) AS accepts_pu
81
+ FROM pu WHERE has_msg GROUP BY is_fast ORDER BY is_fast;
82
+
83
+ SELECT
84
+ (ascii(substr(user_id::VARCHAR, 1, 1)) + ascii(substr(user_id::VARCHAR, -1, 1))) % 5 < 2 AS is_fast,
85
+ COUNT(*) AS n, ROUND(AVG(response_time_hours), 2) AS avg_rt
86
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
87
+ WHERE event = 'message sent' AND response_time_hours IS NOT NULL
88
+ GROUP BY 1 ORDER BY is_fast;
89
+
90
+ -- Hook 6: NEW SELLER CHURN — new_seller post/pre(+14d) event ratio ~0.5x rest
91
+ -- (50% drop stacked on the persona's 28-day activeWindow)
92
+ WITH pu AS (
93
+ SELECT e.user_id::VARCHAR AS uid, ANY_VALUE(u.segment) AS segment, MIN(e.time::TIMESTAMP) AS t0
94
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true) e
95
+ JOIN read_json_auto('data/verify-marketplace-USERS*.json', sample_size=-1, union_by_name=true) u
96
+ ON e.user_id::VARCHAR = u.distinct_id::VARCHAR
97
+ GROUP BY 1
98
+ ), flags AS (
99
+ SELECT p.uid, p.segment,
100
+ COUNT(*) FILTER (WHERE e.time::TIMESTAMP <= p.t0 + INTERVAL 14 DAY) AS pre,
101
+ COUNT(*) FILTER (WHERE e.time::TIMESTAMP > p.t0 + INTERVAL 14 DAY) AS post
102
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true) e
103
+ JOIN pu p ON e.user_id::VARCHAR = p.uid
104
+ GROUP BY 1, 2
105
+ )
106
+ SELECT (segment = 'new_seller') AS is_new_seller, COUNT(*) AS users,
107
+ ROUND(AVG(post::DOUBLE / NULLIF(pre, 0)), 4) AS avg_post_pre
108
+ FROM flags WHERE pre > 0
109
+ GROUP BY (segment = 'new_seller') ORDER BY is_new_seller;
110
+
111
+ -- Hook 7: POWER SELLER PROFILES — tx means 300 / 27.5 / 1.5; ratings
112
+ -- 4.75 / 3.75 / 0; buyers keep the [0] default tx
113
+ SELECT segment, COUNT(*) AS users,
114
+ ROUND(AVG(total_transactions), 1) AS avg_tx,
115
+ ROUND(AVG(seller_rating), 2) AS avg_rating
116
+ FROM read_json_auto('data/verify-marketplace-USERS*.json', sample_size=-1, union_by_name=true)
117
+ GROUP BY segment ORDER BY avg_tx DESC;
118
+
119
+ -- Hook 9: BROWSE-TO-PURCHASE TTC — not visible here; emulator-only read.
120
+ -- See derivation notes above and the timeToConvert assertion in
121
+ -- marketplace.js stories (48h window: frequent/casual ~0.61,
122
+ -- window/casual ~1.12, power/casual ~0.64).
123
+
124
+ -- Hook 10: MESSAGE-COUNT MAGIC NUMBER — per-user avg offer_amount by
125
+ -- total-message cohort: sweet(2-5)/low(0-1) ~1.30x, over(6+)/low ~0.83x
126
+ WITH pu AS (
127
+ SELECT user_id::VARCHAR AS uid,
128
+ COUNT(*) FILTER (WHERE event = 'message sent') AS msgs,
129
+ AVG(offer_amount) FILTER (WHERE event = 'offer received') AS avg_offer
130
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true)
131
+ GROUP BY 1
132
+ )
133
+ SELECT
134
+ CASE WHEN msgs BETWEEN 2 AND 5 THEN 'sweet' WHEN msgs >= 6 THEN 'over' ELSE 'low' END AS bucket,
135
+ COUNT(*) FILTER (WHERE avg_offer IS NOT NULL) AS users,
136
+ ROUND(AVG(avg_offer), 1) AS avg_offer
137
+ FROM pu
138
+ GROUP BY (CASE WHEN msgs BETWEEN 2 AND 5 THEN 'sweet' WHEN msgs >= 6 THEN 'over' ELSE 'low' END)
139
+ ORDER BY bucket;
140
+
141
+ -- Identity invariants: uid coverage 1.0 (auth event is Buyer Onboarding
142
+ -- step 1 — no device-only prefix), device coverage ~0.999, devices/user
143
+ -- ~2.06 (avgDevicePerUser: 2)
144
+ SELECT
145
+ ROUND(AVG((user_id IS NOT NULL)::INT), 4) AS uid_share,
146
+ ROUND(AVG((device_id IS NOT NULL)::INT), 4) AS device_share,
147
+ ROUND(COUNT(DISTINCT device_id)::DOUBLE / COUNT(DISTINCT user_id), 2) AS devices_per_user
148
+ FROM read_json_auto('data/verify-marketplace-EVENTS*.json', sample_size=-1, union_by_name=true);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * marketplace.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of
6
+ * ./marketplace.js; this script just streams the shards and
7
+ * delegates. It is equivalent to:
8
+ *
9
+ * node scripts/verify-stories.mjs dungeons/vertical/marketplace/marketplace.js --data-prefix verify-marketplace
10
+ *
11
+ * Generate first:
12
+ * node scripts/verify-runner.mjs dungeons/vertical/marketplace/marketplace.js verify-marketplace
13
+ * Run:
14
+ * node dungeons/vertical/marketplace/marketplace.verify.mjs [prefix] # default verify-marketplace
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 './marketplace.js';
23
+
24
+ const PREFIX = process.argv[2] || 'verify-marketplace';
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/marketplace/marketplace.js ${PREFIX}`);
45
+ process.exit(1);
46
+ }
47
+ console.log(`marketplace — 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 + purchases-per-cart
56
+ // ratio-of-ratios reads that cancel persona activity confounds); H9
57
+ // dispatches to emulateBreakdown's timeToConvert at a 48-HOUR window —
58
+ // funnel-post gap scaling is invisible to cross-event SQL, and longer
59
+ // windows admit cross-instance chains that contaminate the fast tail.
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);