@ak--47/dungeon-master 1.5.4 → 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 +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  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 +183 -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 +2 -1
  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,175 @@
1
+ -- ============================================================
2
+ -- fintech.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in fintech.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/fintech/fintech.js --data-prefix verify-fintech
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/fintech/fintech.js verify-fintech
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/fintech/fintech.sql"
11
+ -- ============================================================
12
+
13
+ -- ── identity-resolution prelude ─────────────────────────────
14
+ -- avgDevicePerUser: 2 — resolve device_id → canonical distinct_id via the
15
+ -- USERS shards so uniques are identity-correct even for device-only rows.
16
+ CREATE OR REPLACE VIEW users AS
17
+ SELECT * FROM read_json_auto('data/verify-fintech-USERS*.json', sample_size=-1, union_by_name=true);
18
+
19
+ CREATE OR REPLACE VIEW device_map AS
20
+ -- profiles store the device pool under the legacy "anonymousIds" key
21
+ -- (buildIdentityMap in lib/verify/identity.js reads the same field)
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-fintech-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
+
34
+ -- ── H1-business-txn-4x ──────────────────────────────────────
35
+ -- user mix ~ 80/20 personal/business; business avg txn amount ~ 4x personal
36
+ SELECT account_segment, count(*) AS users FROM users GROUP BY 1 ORDER BY 1;
37
+
38
+ SELECT u.account_segment, count(*) AS txns, round(avg(e.amount), 0) AS avg_amount
39
+ FROM ev e JOIN users u ON e.uid = u.distinct_id
40
+ WHERE e.event = 'transaction completed' AND e.amount IS NOT NULL
41
+ GROUP BY 1 ORDER BY avg_amount DESC;
42
+
43
+
44
+ -- ── H2-payday-amounts ───────────────────────────────────────
45
+ -- direct deposits 3x on the 1st/15th; transfers ~1.6x on days 1-3 / 15-17
46
+ SELECT EXTRACT(DAY FROM t) AS dom, count(*) AS deposits, round(avg(amount), 0) AS avg_amount
47
+ FROM ev WHERE event = 'transaction completed' AND transaction_type = 'direct_deposit'
48
+ GROUP BY 1 ORDER BY 1;
49
+
50
+ SELECT CASE WHEN EXTRACT(DAY FROM t) IN (1, 2, 3, 15, 16, 17) THEN 'payday_window' ELSE 'other' END AS bucket,
51
+ count(*) AS transfers, round(avg(amount), 0) AS avg_amount
52
+ FROM ev WHERE event = 'transfer sent' GROUP BY 1 ORDER BY 1;
53
+
54
+
55
+ -- ── H3-fraud-cohort-share ───────────────────────────────────
56
+ -- ~3% of SUPPORT-HISTORY users carry the full burst signature: 3+ rapid
57
+ -- credit purchases, then suspicious lock + unauthorized dispute within 1h.
58
+ -- The bare lock+dispute pair is NOT the detector (organic Support-funnel
59
+ -- reason collisions outnumber the hook ~4:1).
60
+ WITH locks AS (SELECT uid, epoch(t) AS ts FROM ev WHERE event = 'card locked' AND reason = 'suspicious_activity'),
61
+ disputes AS (SELECT uid, epoch(t) AS ts FROM ev WHERE event = 'dispute filed' AND reason = 'unauthorized'),
62
+ txns AS (SELECT uid, epoch(t) AS ts FROM ev WHERE event = 'transaction completed' AND transaction_type = 'purchase' AND payment_method = 'credit'),
63
+ burst_locks AS (
64
+ SELECT l.uid, l.ts FROM locks l JOIN txns x ON x.uid = l.uid AND x.ts BETWEEN l.ts - 3900 AND l.ts
65
+ GROUP BY 1, 2 HAVING count(*) >= 3
66
+ ),
67
+ sig AS (SELECT DISTINCT b.uid FROM burst_locks b JOIN disputes d ON d.uid = b.uid AND d.ts - b.ts BETWEEN 0 AND 3600),
68
+ hist AS (SELECT count(*) AS n FROM (
69
+ SELECT uid FROM ev WHERE event = 'card locked' GROUP BY 1
70
+ INTERSECT
71
+ SELECT uid FROM ev WHERE event = 'dispute filed' GROUP BY 1
72
+ ))
73
+ SELECT (SELECT count(*) FROM sig) AS fraud_users,
74
+ (SELECT n FROM hist) AS support_history_users,
75
+ round((SELECT count(*) FROM sig)::DOUBLE / (SELECT n FROM hist), 4) AS fraction;
76
+
77
+
78
+ -- ── H4-lowbal-churn-suppression ─────────────────────────────
79
+ -- 3+ balance checks under $8K → post-d30/pre-d30 event ratio ~ 0.5x healthy
80
+ WITH low AS (
81
+ SELECT uid, count(*) FILTER (WHERE event = 'balance checked' AND account_balance < 8000) AS low_checks
82
+ FROM ev GROUP BY 1
83
+ ),
84
+ cutoff AS (SELECT min(t) + INTERVAL 30 DAY AS c FROM ev)
85
+ SELECT CASE WHEN l.low_checks >= 3 THEN 'lowbal' ELSE 'healthy' END AS cohort,
86
+ count(DISTINCT e.uid) AS users,
87
+ round((count(*) FILTER (WHERE e.t >= (SELECT c FROM cutoff)))::DOUBLE
88
+ / nullif(count(*) FILTER (WHERE e.t < (SELECT c FROM cutoff)), 0), 2) AS post_pre
89
+ FROM ev e JOIN low l USING (uid) GROUP BY 1 ORDER BY 1;
90
+
91
+
92
+ -- ── H5-budget-discipline ────────────────────────────────────
93
+ -- disciplined budgeters (3+ budgets, ~77% of users): 2x savings
94
+ -- contributions, 1.5x investment amounts vs light budgeters (0-2, ~14%)
95
+ WITH cohort AS (SELECT uid, count(*) FILTER (WHERE event = 'budget created') >= 3 AS disciplined FROM ev GROUP BY 1)
96
+ SELECT CASE WHEN c.disciplined THEN 'disciplined' ELSE 'light' END AS cohort, count(*) AS n,
97
+ round(avg(e.monthly_contribution), 0) AS avg_contribution
98
+ FROM ev e JOIN cohort c USING (uid)
99
+ WHERE e.event = 'savings goal set' AND e.monthly_contribution IS NOT NULL
100
+ GROUP BY 1 ORDER BY 1;
101
+
102
+ WITH cohort AS (SELECT uid, count(*) FILTER (WHERE event = 'budget created') >= 3 AS disciplined FROM ev GROUP BY 1)
103
+ SELECT CASE WHEN c.disciplined THEN 'disciplined' ELSE 'light' END AS cohort, count(*) AS n,
104
+ round(avg(e.amount), 0) AS avg_amount
105
+ FROM ev e JOIN cohort c USING (uid)
106
+ WHERE e.event = 'investment made' AND e.amount IS NOT NULL
107
+ GROUP BY 1 ORDER BY 1;
108
+
109
+
110
+ -- ── H6-autopay-missed-share ─────────────────────────────────
111
+ -- missed/paid ~ 0.22 (60% manual x 30% missed → 0.18/0.82); missed rows are
112
+ -- always auto_pay = false
113
+ SELECT event, auto_pay, count(*) AS n
114
+ FROM ev WHERE event IN ('bill paid', 'bill payment missed')
115
+ GROUP BY 1, 2 ORDER BY 1, 2;
116
+
117
+
118
+ -- ── H7-premium-tier-value ───────────────────────────────────
119
+ -- premium 3x reward value, plus 1.5x; premium 2x investment-sell amount
120
+ SELECT account_tier, count(*) AS redemptions, round(avg("value"), 1) AS avg_value
121
+ FROM ev WHERE event = 'reward redeemed' GROUP BY 1 ORDER BY avg_value DESC;
122
+
123
+ SELECT account_tier, count(*) AS sells, round(avg(amount), 0) AS avg_amount
124
+ FROM ev WHERE event = 'investment made' AND action = 'sell'
125
+ GROUP BY 1 ORDER BY avg_amount DESC;
126
+
127
+
128
+ -- ── H8-month-end-anxiety ────────────────────────────────────
129
+ -- days >= 28: session durations 1.4x, reported balances 0.7x
130
+ SELECT CASE WHEN EXTRACT(DAY FROM t) >= 28 THEN 'monthEnd' ELSE 'other' END AS bucket,
131
+ round(avg(session_duration_sec), 0) AS avg_session_sec, count(*) AS sessions
132
+ FROM ev WHERE event = 'app session' GROUP BY 1 ORDER BY 1;
133
+
134
+ SELECT CASE WHEN EXTRACT(DAY FROM t) >= 28 THEN 'monthEnd' ELSE 'other' END AS bucket,
135
+ round(avg(account_balance), 0) AS avg_balance, count(*) AS checks
136
+ FROM ev WHERE event = 'balance checked' GROUP BY 1 ORDER BY 1;
137
+
138
+
139
+ -- ── H9-onboarding-ttc-by-tier ───────────────────────────────
140
+ -- cross-event MIN→MIN TTC (account opened → first balance checked after it):
141
+ -- MEDIAN basic ~ 2x premium (factors 1.33 vs 0.67), plus between. Median,
142
+ -- not avg — TTC is heavy-tailed and a single multi-day straggler dominates
143
+ -- the mean of these small cohorts (only born-in users have "account opened").
144
+ WITH ao AS (SELECT uid, min(t) AS t0 FROM ev WHERE event = 'account opened' GROUP BY 1),
145
+ bc AS (
146
+ SELECT e.uid, min(e.t) AS t1
147
+ FROM ev e JOIN ao ON e.uid = ao.uid AND e.t >= ao.t0
148
+ WHERE e.event = 'balance checked' GROUP BY 1
149
+ )
150
+ SELECT u.account_tier, count(*) AS converters,
151
+ round(median(epoch(t1 - t0)) / 60, 1) AS median_ttc_min,
152
+ round(avg(epoch(t1 - t0)) / 60, 1) AS avg_ttc_min
153
+ FROM ao JOIN bc USING (uid) JOIN users u ON u.distinct_id = ao.uid
154
+ GROUP BY 1 ORDER BY median_ttc_min DESC;
155
+
156
+
157
+ -- ── H10-txn-magic-number ────────────────────────────────────
158
+ -- sweet band (12-19 txns, measured: median 12 / p75 17 / p90 20) invests
159
+ -- 1.4x the 1-11 band; over band (20+) has 0.8x the premium-upgrade share of
160
+ -- NON-TXN events (raw per-user counts RISE with activity, and the over band
161
+ -- is selected for high txn counts — normalizing by non-txn events removes
162
+ -- both distortions)
163
+ WITH txn AS (SELECT uid, count(*) AS n FROM ev WHERE event = 'transaction completed' GROUP BY 1),
164
+ bands AS (SELECT uid, CASE WHEN n BETWEEN 12 AND 19 THEN 'sweet' WHEN n < 12 THEN 'low' ELSE 'over' END AS band FROM txn)
165
+ SELECT b.band, count(DISTINCT e.uid) AS users, round(avg(e.amount), 0) AS avg_invest_amount
166
+ FROM ev e JOIN bands b USING (uid)
167
+ WHERE e.event = 'investment made' AND e.amount IS NOT NULL
168
+ GROUP BY 1 ORDER BY 1;
169
+
170
+ WITH txn AS (SELECT uid, count(*) AS n FROM ev WHERE event = 'transaction completed' GROUP BY 1),
171
+ bands AS (SELECT uid, CASE WHEN n BETWEEN 12 AND 19 THEN 'sweet' WHEN n < 12 THEN 'low' ELSE 'over' END AS band FROM txn)
172
+ SELECT b.band, count(DISTINCT e.uid) AS users,
173
+ round((count(*) FILTER (WHERE e.event = 'premium upgraded'))::DOUBLE
174
+ / (count(*) FILTER (WHERE e.event != 'transaction completed')), 5) AS upgrade_share
175
+ FROM ev e JOIN bands b USING (uid) GROUP BY 1 ORDER BY 1;
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fintech.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./fintech.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/fintech/fintech.js --data-prefix verify-fintech
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/fintech/fintech.js verify-fintech
12
+ * Run:
13
+ * node dungeons/vertical/fintech/fintech.verify.mjs [prefix] # default verify-fintech
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 './fintech.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-fintech';
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/fintech/fintech.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`fintech — 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) — every timeToConvert story carries an
55
+ // explicit conversionWindowMs, so no funnel-default threading is needed
56
+ const results = await evaluateStories(stories, events, {
57
+ profiles,
58
+ funnels: config.funnels,
59
+ identityMap: buildIdentityMap(profiles),
60
+ runSql,
61
+ });
62
+
63
+ let worst = 'NAILED';
64
+ for (const r of results) {
65
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
66
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
67
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
68
+ }
69
+ console.log(`\nworst verdict: ${worst}`);
70
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);