@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,193 @@
1
+ -- ============================================================
2
+ -- healthcare.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in healthcare.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/healthcare/healthcare.js --data-prefix verify-healthcare
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/healthcare/healthcare.js verify-healthcare
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/healthcare/healthcare.sql"
11
+ -- ============================================================
12
+
13
+ -- ── identity-resolution prelude ─────────────────────────────
14
+ -- avgDevicePerUser: 2 + account created is both isAuthEvent and
15
+ -- isFirstEvent, so born users auth on their first event; the device-pool
16
+ -- resolve is belt-and-braces for any device-only edge.
17
+ CREATE OR REPLACE VIEW users AS
18
+ SELECT * FROM read_json_auto('data/verify-healthcare-USERS*.json', sample_size=-1, union_by_name=true);
19
+
20
+ CREATE OR REPLACE VIEW device_map AS
21
+ -- profiles store the device pool under the legacy "anonymousIds" key
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-healthcare-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
+ -- Per-user consultation counts. H3/H10 classify on counts taken after all
34
+ -- filters (H8 cliff, H6 thinning) and nothing drops consultations later,
35
+ -- so output-side counts rebuild the hook cohorts exactly.
36
+ CREATE OR REPLACE VIEW consult_ct AS
37
+ SELECT uid, count(*) AS ct FROM ev WHERE event = 'consultation completed' GROUP BY 1;
38
+
39
+
40
+ -- ── H1-after-hours-pricing ──────────────────────────────────
41
+ -- consultations 19:00-07:00 UTC carry consultation_fee × 1.5. H10's
42
+ -- sweet-spot boost rides both HOD bins equally, so avg AND median ratios
43
+ -- read the knob.
44
+ SELECT CASE WHEN extract(hour FROM t) >= 19 OR extract(hour FROM t) < 7 THEN 'after' ELSE 'business' END AS grp,
45
+ count(*) AS consults, round(avg(consultation_fee), 2) AS avg_fee, median(consultation_fee) AS med_fee
46
+ FROM ev WHERE event = 'consultation completed' GROUP BY 1 ORDER BY 1;
47
+
48
+
49
+ -- ── H2-flu-season ───────────────────────────────────────────
50
+ -- days 50-70 (2026-02-20 → 2026-03-12): bookings forced respiratory at
51
+ -- 60% (expected share 0.60 + 0.40 × 1/8 = 0.65 vs declared 0.125) and
52
+ -- in-window respiratory wait_time_hours × 2.
53
+ SELECT CASE WHEN t > TIMESTAMP '2026-02-20' AND t < TIMESTAMP '2026-03-12' THEN 'in' ELSE 'out' END AS grp,
54
+ count(*) AS bookings,
55
+ round(count(*) FILTER (WHERE condition_type = 'respiratory')::DOUBLE / count(*), 4) AS resp_share,
56
+ round(avg(wait_time_hours) FILTER (WHERE condition_type = 'respiratory'), 1) AS resp_wait,
57
+ round(avg(wait_time_hours) FILTER (WHERE condition_type <> 'respiratory'), 1) AS other_wait
58
+ FROM ev WHERE event = 'appointment booked' GROUP BY 1 ORDER BY 1;
59
+
60
+
61
+ -- ── H3-experienced-doctor-satisfaction ──────────────────────
62
+ -- users with >12 output consultations: every satisfaction_score redrawn
63
+ -- uniform [4.0, 5.0]. Purity is exact (later hooks only delete consults) —
64
+ -- below_min must be 0.
65
+ SELECT count(*) FILTER (WHERE e.satisfaction_score < 4.0) AS below_min,
66
+ count(*) AS scores, count(DISTINCT c.uid) AS exp_users
67
+ FROM consult_ct c JOIN ev e ON e.uid = c.uid AND e.event = 'consultation completed'
68
+ WHERE c.ct > 12;
69
+
70
+ SELECT CASE WHEN c.ct > 12 THEN 'exp' WHEN c.ct <= 9 THEN 'base' ELSE 'mid' END AS grp,
71
+ count(DISTINCT c.uid) AS users, round(avg(e.satisfaction_score), 2) AS avg_sat
72
+ FROM consult_ct c JOIN ev e ON e.uid = c.uid AND e.event = 'consultation completed'
73
+ GROUP BY 1 ORDER BY avg_sat;
74
+
75
+
76
+ -- ── H4-video-followup-lift ──────────────────────────────────
77
+ -- 60% of video consultations inject one cloned follow-up 1-7d later.
78
+ -- Per-consultation attribution: follow-ups within 7d after each consult,
79
+ -- video minus phone ≈ 0.6 (attenuated by clones landing in neighboring
80
+ -- phone consults' windows). Restricted to consults ≥7d before datasetEnd
81
+ -- (clones past the end are future-guard dropped) among users with ≥1
82
+ -- follow-up (clones need an organic template).
83
+ WITH fu_users AS (SELECT DISTINCT uid FROM ev WHERE event = 'follow up scheduled'),
84
+ cons AS (SELECT e.uid, e.t, e.consultation_mode AS mode
85
+ FROM ev e JOIN fu_users f ON f.uid = e.uid
86
+ WHERE e.event = 'consultation completed' AND e.t <= TIMESTAMP '2026-04-24 23:59:59'),
87
+ cnt AS (SELECT c.uid, c.mode, c.t, count(fu.uid) AS fu7
88
+ FROM cons c LEFT JOIN ev fu ON fu.uid = c.uid AND fu.event = 'follow up scheduled'
89
+ AND fu.t > c.t AND fu.t <= c.t + INTERVAL 7 DAY
90
+ GROUP BY 1, 2, 3)
91
+ SELECT mode AS grp, count(*) AS consults, round(avg(fu7), 3) AS avg_fu_within_7d
92
+ FROM cnt GROUP BY 1 ORDER BY 1;
93
+
94
+
95
+ -- ── H5-chronic-refill-chain ─────────────────────────────────
96
+ -- each chronic prescription spawns 2-4 cloned refills at ~30d intervals
97
+ -- (chronic / chronic_maintenance / refill_count=i); clones past
98
+ -- datasetEnd are future-guard dropped. Inspect: refill mix on chronic-rx
99
+ -- users vs everyone else (organic chronic∧chronic_maintenance mix is
100
+ -- 2/7 × 1/8 ≈ 0.036).
101
+ WITH refill_users AS (SELECT DISTINCT uid FROM ev WHERE event = 'prescription refill'),
102
+ cohort AS (SELECT DISTINCT e.uid FROM ev e JOIN refill_users ru ON ru.uid = e.uid
103
+ WHERE e.event = 'prescription issued' AND e.condition_type = 'chronic')
104
+ SELECT (c.uid IS NOT NULL) AS chronic_rx_user, count(*) AS refills,
105
+ round(count(*) FILTER (WHERE e.condition_type = 'chronic' AND e.medication_type = 'chronic_maintenance')::DOUBLE / count(*), 4) AS cm_share
106
+ FROM ev e LEFT JOIN cohort c ON c.uid = e.uid
107
+ WHERE e.event = 'prescription refill' GROUP BY 1 ORDER BY 1;
108
+
109
+
110
+ -- ── H6-occasional-no-shows ──────────────────────────────────
111
+ -- users with <15 events (hook-time) lose 25% of consultations and get
112
+ -- no_show=true on 25% of bookings. Flag purity is exact: flagged ⇒ output
113
+ -- count ≤ 14 (everything after only deletes), so ≥15-event users must
114
+ -- carry ZERO no_show=true rows.
115
+ WITH tot AS (SELECT uid, count(*) AS ct FROM ev GROUP BY 1)
116
+ SELECT (t2.ct >= 15) AS big_user, count(*) AS bookings,
117
+ count(*) FILTER (WHERE e.no_show = true) AS noshows,
118
+ round(count(*) FILTER (WHERE e.no_show = true)::DOUBLE / count(*), 4) AS ns_rate
119
+ FROM ev e JOIN tot t2 ON t2.uid = e.uid
120
+ WHERE e.event = 'appointment booked' GROUP BY 1 ORDER BY 1;
121
+
122
+
123
+ -- ── H7-doctor-specialization ────────────────────────────────
124
+ -- user hook: doctors get specialty + years_experience uniform [15, 30];
125
+ -- nurses [3, 15]; patients pinned 0. Ranges are deterministic — min/max
126
+ -- outside them is a hook bug.
127
+ SELECT role, count(*) AS users,
128
+ round(avg(years_experience), 1) AS avg_yx, min(years_experience) AS min_yx, max(years_experience) AS max_yx,
129
+ count(*) FILTER (WHERE specialty = 'none') AS specialty_none
130
+ FROM users GROUP BY 1 ORDER BY avg_yx DESC;
131
+
132
+
133
+ -- ── H8-free-tier-cliff ──────────────────────────────────────
134
+ -- 30% of free-tier users lose ALL consultations (per-user cliff).
135
+ -- (z_free − z_paid) / (1 − z_paid) reads the knob with the natural-zero
136
+ -- baseline cancelled.
137
+ WITH per AS (SELECT u.distinct_id::VARCHAR AS uid, u.subscription_tier AS tier FROM users u),
138
+ cons AS (SELECT uid, count(*) AS ct FROM ev WHERE event = 'consultation completed' GROUP BY 1),
139
+ j AS (SELECT p.tier, coalesce(c.ct, 0) AS ct FROM per p LEFT JOIN cons c ON c.uid = p.uid)
140
+ SELECT CASE WHEN tier = 'free' THEN 'free' ELSE 'paid' END AS grp, count(*) AS users,
141
+ round(count(*) FILTER (WHERE ct = 0)::DOUBLE / count(*), 4) AS zero_consult_share,
142
+ round(avg(ct) FILTER (WHERE ct > 0), 2) AS avg_consults_survivors
143
+ FROM j GROUP BY 1 ORDER BY 1;
144
+
145
+ -- Survivor placebo (cliff vs per-event thinning), SEGMENT-STANDARDIZED:
146
+ -- tier and persona come out correlated in-sample and persona
147
+ -- eventModifier drives volume, so the raw survivor comparison above is
148
+ -- confounded by composition (visible in avg_consults_survivors). Within
149
+ -- segment, free survivors ≈ basic (ratio ~1.0); thinning would read ~0.7
150
+ -- in every segment.
151
+ WITH cons AS (SELECT uid, count(*) AS ct FROM ev WHERE event = 'consultation completed' GROUP BY 1),
152
+ surv AS (SELECT u.subscription_tier AS tier, u.segment AS seg, c.ct
153
+ FROM users u JOIN cons c ON c.uid = u.distinct_id::VARCHAR)
154
+ SELECT seg, count(*) FILTER (WHERE tier = 'free') AS free_n, count(*) FILTER (WHERE tier = 'basic') AS basic_n,
155
+ round(avg(ct) FILTER (WHERE tier = 'free'), 2) AS free_avg,
156
+ round(avg(ct) FILTER (WHERE tier = 'basic'), 2) AS basic_avg,
157
+ round(avg(ct) FILTER (WHERE tier = 'free') / avg(ct) FILTER (WHERE tier = 'basic'), 4) AS ratio
158
+ FROM surv GROUP BY 1 ORDER BY 1;
159
+
160
+
161
+ -- ── H9-ttc-by-tier ──────────────────────────────────────────
162
+ -- premium × 0.67 / free × 1.4 on wait_time_hours, duration_minutes, and
163
+ -- the first booked→consult→follow-up sequence's timestamps. Property
164
+ -- ratios read the knobs exactly (H2's flu doubling is tier-blind and
165
+ -- cancels in the ratio).
166
+ SELECT subscription_tier, count(*) AS bookings, round(avg(wait_time_hours), 1) AS avg_wait
167
+ FROM ev WHERE event = 'appointment booked' GROUP BY 1 ORDER BY avg_wait;
168
+
169
+ SELECT subscription_tier, count(*) AS consults, round(avg(duration_minutes), 1) AS avg_dur
170
+ FROM ev WHERE event = 'consultation completed' GROUP BY 1 ORDER BY avg_dur;
171
+
172
+ -- CAUTION: cross-event TTC SQL here would be censored by its lookback
173
+ -- window (the ai-platform H9 lesson — censoring can invert the measured
174
+ -- direction). The story asserts TTC through the Mixpanel-aligned emulator
175
+ -- at a 2016h window (= 1.4 × 2 gaps × 30d per-gap cap, covering the
176
+ -- stretched support); trust the story verdict, not ad-hoc pair SQL.
177
+
178
+
179
+ -- ── H10-consult-count-magic-number ──────────────────────────
180
+ -- sweet 3-6 consults → consultation_fee × 1.25; over 7+ → phone-mode
181
+ -- days_until_followup × 1.5 (phone filter excludes H4's video clones,
182
+ -- whose different base distribution would confound). Median ratios are
183
+ -- selection-free: whole-cohort iid scaling moves every quantile by the
184
+ -- knob.
185
+ SELECT CASE WHEN c.ct BETWEEN 3 AND 6 THEN 'sweet' WHEN c.ct >= 7 THEN 'over' ELSE 'low' END AS grp,
186
+ count(DISTINCT c.uid) AS users, median(e.consultation_fee) AS med_fee
187
+ FROM consult_ct c JOIN ev e ON e.uid = c.uid AND e.event = 'consultation completed'
188
+ GROUP BY 1 ORDER BY med_fee;
189
+
190
+ SELECT CASE WHEN c.ct BETWEEN 3 AND 6 THEN 'sweet' WHEN c.ct >= 7 THEN 'over' ELSE 'low' END AS grp,
191
+ count(DISTINCT c.uid) AS users, median(e.days_until_followup) AS med_days
192
+ FROM consult_ct c JOIN ev e ON e.uid = c.uid AND e.event = 'follow up scheduled' AND e.consultation_mode = 'phone'
193
+ GROUP BY 1 ORDER BY med_days;
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * healthcare.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./healthcare.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/healthcare/healthcare.js --data-prefix verify-healthcare
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/healthcare/healthcare.js verify-healthcare
12
+ * Run:
13
+ * node dungeons/vertical/healthcare/healthcare.verify.mjs [prefix] # default verify-healthcare
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 './healthcare.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-healthcare';
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/healthcare/healthcare.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`healthcare — 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) — the H8/H9 stories carry their own
55
+ // explicit conversion windows (derived from the funnel's generative window
56
+ // × the max H9 stretch factor, covering the stretched support), so no
57
+ // funnel-default threading is needed
58
+ const results = await evaluateStories(stories, events, {
59
+ profiles,
60
+ funnels: config.funnels,
61
+ identityMap: buildIdentityMap(profiles),
62
+ runSql,
63
+ });
64
+
65
+ let worst = 'NAILED';
66
+ for (const r of results) {
67
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
68
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
69
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
70
+ }
71
+ console.log(`\nworst verdict: ${worst}`);
72
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);