@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.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- 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);
|