@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.
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 +96 -0
  4. package/.claude/skills/create-project/provision.mjs +336 -0
  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 +211 -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 +3 -2
  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,183 @@
1
+ -- ============================================================
2
+ -- food-delivery.js — v1.6.0 hook verification queries (human eyeball)
3
+ -- Machine contract lives in the `stories` export of food-delivery.js;
4
+ -- run ./food-delivery.verify.mjs for verdicts. These queries mirror the
5
+ -- story reads for interactive inspection.
6
+ --
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/food-delivery/food-delivery.js verify-food-delivery
9
+ --
10
+ -- Key derivation notes (full math in HOOK STORIES block of food-delivery.js):
11
+ -- - H1/H7 read delivered-per-PLACED ratios: 'order placed' shares the soup
12
+ -- HOD/hash distribution but is untouched by either drop, so it cancels
13
+ -- the confound. Raw delivered HOD volume does NOT show a clean 0.70.
14
+ -- - H6 zero-post share lands near 0.25 (not the 0.60 knob): the deletion
15
+ -- removes late 'subscription started' events themselves, so churned
16
+ -- users with a late trial start vanish from the visible cohort.
17
+ -- - H8 buckets by HOOK-TIME order count; output counts are contaminated by
18
+ -- H4 duplication and H6 deletion, so the clean read excludes rainy-window
19
+ -- orderers and users with zero post-day-14 activity.
20
+ -- - H10 reads reorders-per-DELIVERED: last funnel step fires only on
21
+ -- conversion (determineConversion, lib/generators/funnels.js), so the
22
+ -- ratio recovers the conversionRate scaling exactly (1.40 / 0.70).
23
+ -- ============================================================
24
+
25
+
26
+ -- H1: RUSH-HOUR KEEP RATE — delivered-per-placed off/rush ≈ 0.70
27
+ WITH ev AS (
28
+ SELECT event, hour(time::TIMESTAMP) AS hr
29
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
30
+ WHERE event IN ('order delivered','order placed')
31
+ ), agg AS (
32
+ SELECT (hr BETWEEN 11 AND 13) OR (hr BETWEEN 17 AND 20) AS rush,
33
+ count(*) FILTER (WHERE event='order delivered') AS del,
34
+ count(*) FILTER (WHERE event='order placed') AS placed
35
+ FROM ev GROUP BY 1
36
+ )
37
+ SELECT rush, del, placed, round(del::DOUBLE/placed, 3) AS del_per_placed FROM agg ORDER BY rush;
38
+
39
+
40
+ -- H2: COUPON INJECTION — coupons-per-checkout diff ≈ +0.30 (Free − QB+); cpu ratio ≈ 2.2
41
+ WITH pu AS (
42
+ SELECT user_id::VARCHAR AS uid, any_value(subscription_tier) AS tier,
43
+ count(*) FILTER (WHERE event='coupon applied') AS coupons,
44
+ count(*) FILTER (WHERE event='checkout started') AS checkouts
45
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
46
+ WHERE event IN ('coupon applied','checkout started') GROUP BY 1
47
+ )
48
+ SELECT tier, count(*) AS users,
49
+ round(sum(coupons)::DOUBLE/count(*), 2) AS coupons_per_user,
50
+ round(sum(coupons)::DOUBLE/nullif(sum(checkouts),0), 3) AS coupons_per_checkout
51
+ FROM pu GROUP BY tier ORDER BY tier;
52
+
53
+
54
+ -- H3: LATE-NIGHT MUNCHIES — flip inversion (late−off)/(1−off) ≈ 0.70; price ratio ≈ 1.30
55
+ WITH ev AS (
56
+ SELECT event, cuisine_type, TRY_CAST(item_price AS DOUBLE) AS price,
57
+ hour(time::TIMESTAMP) AS hr
58
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
59
+ WHERE event IN ('restaurant viewed','restaurant browsed','item added to cart')
60
+ )
61
+ SELECT
62
+ round(avg((cuisine_type='American')::INT) FILTER (WHERE event='restaurant viewed' AND (hr>=22 OR hr<=2)), 3) AS amer_late,
63
+ round(avg((cuisine_type='American')::INT) FILTER (WHERE event='restaurant viewed' AND hr BETWEEN 6 AND 18), 3) AS amer_off,
64
+ round(avg((cuisine_type='American')::INT) FILTER (WHERE event='restaurant browsed'), 3) AS amer_organic_ctrl,
65
+ round(avg(price) FILTER (WHERE event='item added to cart' AND (hr>=22 OR hr<=2)) /
66
+ avg(price) FILTER (WHERE event='item added to cart' AND hr BETWEEN 6 AND 18), 3) AS price_ratio
67
+ FROM ev;
68
+
69
+
70
+ -- H4: RAINY WEEK (day_idx 20-27) — dup share ≈ 0.40, fee ratio ≈ 2.0, vol RoR vs checkout ≈ 1.4
71
+ WITH ev AS (
72
+ SELECT user_id::VARCHAR AS uid, event, order_id, TRY_CAST(delivery_fee AS DOUBLE) AS fee,
73
+ date_diff('day', TIMESTAMP '2026-01-01 00:00:00', time::TIMESTAMP) AS day_idx
74
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
75
+ WHERE event IN ('order placed','checkout started')
76
+ )
77
+ SELECT
78
+ round((count(*) FILTER (WHERE event='order placed' AND day_idx BETWEEN 20 AND 27)
79
+ - count(DISTINCT uid || '|' || order_id) FILTER (WHERE event='order placed' AND day_idx BETWEEN 20 AND 27))::DOUBLE
80
+ / count(DISTINCT uid || '|' || order_id) FILTER (WHERE event='order placed' AND day_idx BETWEEN 20 AND 27), 3) AS dup_share,
81
+ round(avg(fee) FILTER (WHERE event='order placed' AND day_idx BETWEEN 20 AND 27) /
82
+ avg(fee) FILTER (WHERE event='order placed' AND (day_idx BETWEEN 10 AND 19 OR day_idx BETWEEN 28 AND 37)), 3) AS fee_ratio,
83
+ round((count(*) FILTER (WHERE event='order placed' AND day_idx BETWEEN 20 AND 27)::DOUBLE /
84
+ count(*) FILTER (WHERE event='order placed' AND (day_idx BETWEEN 10 AND 19 OR day_idx BETWEEN 28 AND 37))) /
85
+ (count(*) FILTER (WHERE event='checkout started' AND day_idx BETWEEN 20 AND 27)::DOUBLE /
86
+ count(*) FILTER (WHERE event='checkout started' AND (day_idx BETWEEN 10 AND 19 OR day_idx BETWEEN 28 AND 37))), 3) AS vol_ror
87
+ FROM ev;
88
+
89
+
90
+ -- H5: REFERRAL POWER USERS (born-in only) — reorders/user ratio ≈ 1.4; ratings 4.4 vs 2.8
91
+ WITH ev AS (
92
+ SELECT user_id::VARCHAR AS uid, event, referral_code, TRY_CAST(food_rating AS DOUBLE) AS rating
93
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
94
+ WHERE event IN ('account created','reorder initiated','order rated')
95
+ ), born AS (
96
+ SELECT uid, bool_or(referral_code = true) AS referred
97
+ FROM ev WHERE event='account created' GROUP BY 1
98
+ ), pu AS (
99
+ SELECT b.uid, b.referred,
100
+ count(*) FILTER (WHERE e.event='reorder initiated') AS reorders,
101
+ avg(e.rating) FILTER (WHERE e.event='order rated') AS user_rating
102
+ FROM born b LEFT JOIN ev e ON e.uid=b.uid GROUP BY 1,2
103
+ )
104
+ SELECT referred, count(*) AS users,
105
+ round(sum(reorders)::DOUBLE/count(*), 2) AS reorders_per_user,
106
+ round(avg(user_rating), 2) AS mean_user_rating
107
+ FROM pu GROUP BY referred ORDER BY referred;
108
+
109
+
110
+ -- H6: TRIAL CHURN (survivor-biased view) — zero-post share ~0.25 nonactivated vs ~0.04 activated
111
+ WITH ev AS (
112
+ SELECT user_id::VARCHAR AS uid, time::TIMESTAMP AS t, event, trial
113
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
114
+ ), fu AS (SELECT uid, min(t) AS first_t FROM ev GROUP BY 1),
115
+ trial AS (SELECT uid FROM ev WHERE event='subscription started' AND trial = true GROUP BY 1),
116
+ pux AS (
117
+ SELECT f.uid,
118
+ count(*) FILTER (WHERE e.event='order placed' AND e.t <= f.first_t + INTERVAL '14 days') AS early_orders,
119
+ count(*) FILTER (WHERE e.t > f.first_t + INTERVAL '14 days') AS post_n
120
+ FROM fu f JOIN trial tr ON tr.uid=f.uid JOIN ev e ON e.uid=f.uid GROUP BY 1
121
+ )
122
+ SELECT (early_orders >= 3) AS activated, count(*) AS users,
123
+ round(avg(CASE WHEN post_n = 0 THEN 1.0 ELSE 0 END), 3) AS zero_post_share
124
+ FROM pux GROUP BY 1 ORDER BY 1;
125
+
126
+
127
+ -- H7: HASH-BUCKET DROP — delivered-per-placed odd/even ≈ 0.70
128
+ WITH pu AS (
129
+ SELECT user_id::VARCHAR AS uid, (ascii(substr(user_id::VARCHAR,1,1)) % 2 = 0) AS even_bucket,
130
+ count(*) FILTER (WHERE event='order placed') AS placed,
131
+ count(*) FILTER (WHERE event='order delivered') AS delivered
132
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
133
+ WHERE event IN ('order placed','order delivered') GROUP BY 1,2
134
+ )
135
+ SELECT even_bucket, count(*) AS users,
136
+ round(sum(delivered)::DOUBLE/nullif(sum(placed),0), 3) AS del_per_placed
137
+ FROM pu GROUP BY even_bucket ORDER BY even_bucket;
138
+
139
+
140
+ -- H8: ORDER-COUNT MAGIC NUMBER (clean population) — sweet/base ≈ 1.40, over/sweet ≈ 0.46
141
+ WITH raw AS (
142
+ SELECT user_id::VARCHAR AS uid, time::TIMESTAMP AS t, event,
143
+ TRY_CAST(order_total AS DOUBLE) AS ot,
144
+ date_diff('day', TIMESTAMP '2026-01-01 00:00:00', time::TIMESTAMP) AS day_idx
145
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
146
+ ), fu AS (SELECT uid, min(t) AS first_t FROM raw GROUP BY 1),
147
+ flags AS (
148
+ SELECT r.uid,
149
+ max(CASE WHEN r.event='order placed' AND r.day_idx BETWEEN 20 AND 27 THEN 1 ELSE 0 END) AS rainy,
150
+ max(CASE WHEN r.t > f.first_t + INTERVAL '14 days' THEN 1 ELSE 0 END) AS alive,
151
+ count(*) FILTER (WHERE r.event='order placed') AS orders,
152
+ sum(r.ot) FILTER (WHERE r.event='order placed') AS spend
153
+ FROM raw r JOIN fu f USING(uid) GROUP BY 1
154
+ )
155
+ SELECT CASE WHEN orders BETWEEN 4 AND 8 THEN 'sweet' WHEN orders >= 9 THEN 'over' ELSE 'base' END AS bucket,
156
+ count(*) AS users, round(sum(spend)/nullif(sum(orders),0), 1) AS avg_order_total
157
+ FROM flags WHERE rainy = 0 AND alive = 1 AND orders > 0
158
+ GROUP BY 1 ORDER BY 1;
159
+
160
+
161
+ -- H9: TIER DELIVERY SPEED — QB+/Free property ratio ≈ 0.479 (= 0.67/1.4) on both props
162
+ SELECT
163
+ round(avg(TRY_CAST(actual_delivery_mins AS DOUBLE)) FILTER (WHERE subscription_tier='QuickBite+' AND event='order delivered') /
164
+ avg(TRY_CAST(actual_delivery_mins AS DOUBLE)) FILTER (WHERE subscription_tier='Free' AND event='order delivered'), 3) AS adm_ratio,
165
+ round(avg(TRY_CAST(eta_mins AS DOUBLE)) FILTER (WHERE subscription_tier='QuickBite+' AND event='order tracked') /
166
+ avg(TRY_CAST(eta_mins AS DOUBLE)) FILTER (WHERE subscription_tier='Free' AND event='order tracked'), 3) AS eta_ratio
167
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
168
+ WHERE event IN ('order delivered','order tracked');
169
+
170
+
171
+ -- H10: CITY DENSITY — reorders-per-delivered dense/base ≈ 1.40, sprawl/base ≈ 0.70
172
+ WITH pu AS (
173
+ SELECT user_id::VARCHAR AS uid, any_value(city) AS city,
174
+ count(*) FILTER (WHERE event='order delivered') AS delivered,
175
+ count(*) FILTER (WHERE event='reorder initiated') AS reorders
176
+ FROM read_json_auto('data/verify-food-delivery-EVENTS*.json', sample_size=-1, union_by_name=true)
177
+ WHERE event IN ('order delivered','reorder initiated') GROUP BY 1
178
+ )
179
+ SELECT CASE WHEN city IN ('San Francisco','New York') THEN 'dense'
180
+ WHEN city IN ('Houston','Phoenix') THEN 'sprawl' ELSE 'base' END AS grp,
181
+ count(*) AS users,
182
+ round(sum(reorders)::DOUBLE/nullif(sum(delivered),0), 4) AS reorders_per_delivered
183
+ FROM pu GROUP BY 1 ORDER BY 1;
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * food-delivery.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of
6
+ * ./food-delivery.js; this script just streams the shards and
7
+ * delegates. It is equivalent to:
8
+ *
9
+ * node scripts/verify-stories.mjs dungeons/vertical/food-delivery/food-delivery.js --data-prefix verify-food-delivery
10
+ *
11
+ * Generate first:
12
+ * node scripts/verify-runner.mjs dungeons/vertical/food-delivery/food-delivery.js verify-food-delivery
13
+ * Run:
14
+ * node dungeons/vertical/food-delivery/food-delivery.verify.mjs [prefix] # default verify-food-delivery
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 './food-delivery.js';
23
+
24
+ const PREFIX = process.argv[2] || 'verify-food-delivery';
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/food-delivery/food-delivery.js ${PREFIX}`);
45
+ process.exit(1);
46
+ }
47
+ console.log(`food-delivery — 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
+ // All stories are DuckDB-backed. Every read here is a ratio-of-ratios
56
+ // (delivered-per-placed, reorders-per-delivered, coupons-per-checkout) or a
57
+ // behavioral-cohort aggregation (hash bucket, hook-time order-count bucket,
58
+ // trial-activation split) — shapes emulateBreakdown's segmentation types
59
+ // don't model, and the RoR normalization is exactly what cancels the soup
60
+ // confounds a raw emulator read would inherit.
61
+ const results = await evaluateStories(stories, events, {
62
+ profiles,
63
+ funnels: config.funnels,
64
+ identityMap: buildIdentityMap(profiles),
65
+ runSql,
66
+ });
67
+
68
+ let worst = 'NAILED';
69
+ for (const r of results) {
70
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
71
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
72
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
73
+ }
74
+ console.log(`\nworst verdict: ${worst}`);
75
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);