@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,167 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- real-estate.js — v1.6 human-inspection queries (DuckDB)
|
|
3
|
+
--
|
|
4
|
+
-- Every query is keyed to a story id in real-estate.js's `stories` export;
|
|
5
|
+
-- the machine-checked verdicts come from:
|
|
6
|
+
-- node scripts/verify-stories.mjs dungeons/vertical/real-estate/real-estate.js --data-prefix verify-real-estate
|
|
7
|
+
-- Generate first:
|
|
8
|
+
-- node scripts/verify-runner.mjs dungeons/vertical/real-estate/real-estate.js verify-real-estate
|
|
9
|
+
-- Run this file:
|
|
10
|
+
-- duckdb -c ".read dungeons/vertical/real-estate/real-estate.sql"
|
|
11
|
+
--
|
|
12
|
+
-- NOTE: this dungeon uses a LITERAL historical window (datasetStart
|
|
13
|
+
-- 2026-01-01 → datasetEnd 2026-05-01, no forward shift), so
|
|
14
|
+
-- day_idx = date_diff('day', DATE '2026-01-01', t::DATE) matches the
|
|
15
|
+
-- hook's day offsets exactly. H10's TTC-by-tier read is emulator-only
|
|
16
|
+
-- (timeToConvert in the stories) — greedy SQL pairing can't isolate the
|
|
17
|
+
-- funnel-instance the hook stretched, so there is no SQL twin here.
|
|
18
|
+
-- ============================================================
|
|
19
|
+
|
|
20
|
+
-- ── identity-resolution prelude ─────────────────────────────
|
|
21
|
+
-- avgDevicePerUser: 2 + 'account created' is both isAuthEvent and
|
|
22
|
+
-- isFirstEvent, so every user auths on their very first event; the
|
|
23
|
+
-- device-pool resolve is belt-and-braces for any device-only edge.
|
|
24
|
+
CREATE OR REPLACE VIEW users AS
|
|
25
|
+
SELECT * FROM read_json_auto('data/verify-real-estate-USERS*.json', sample_size=-1, union_by_name=true);
|
|
26
|
+
|
|
27
|
+
CREATE OR REPLACE VIEW device_map AS
|
|
28
|
+
-- profiles store the device pool under the legacy "anonymousIds" key
|
|
29
|
+
SELECT unnest("anonymousIds") AS device_id, distinct_id FROM users;
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE VIEW ev AS
|
|
32
|
+
-- ::VARCHAR casts — user_id sniffs as UUID, device_id as VARCHAR; DuckDB
|
|
33
|
+
-- refuses to coalesce mixed types
|
|
34
|
+
SELECT coalesce(m.distinct_id::VARCHAR, e.user_id::VARCHAR, e.device_id::VARCHAR) AS uid,
|
|
35
|
+
e.time::TIMESTAMP AS t,
|
|
36
|
+
date_diff('day', DATE '2026-01-01', e.time::TIMESTAMP::DATE) AS day_idx,
|
|
37
|
+
e.*
|
|
38
|
+
FROM read_json_auto('data/verify-real-estate-EVENTS*.json', sample_size=-1, union_by_name=true) e
|
|
39
|
+
LEFT JOIN device_map m ON e.device_id = m.device_id;
|
|
40
|
+
|
|
41
|
+
-- Per-user aggregates. Output-cohort classification is exact for the
|
|
42
|
+
-- behavioral cohorts below: H8 (cold lead) classifies after all first-14d
|
|
43
|
+
-- view/save mutations are in the array, H9's view count is untouched by
|
|
44
|
+
-- later hooks, and H3's first-7d saved-search events are never deleted.
|
|
45
|
+
CREATE OR REPLACE VIEW pu AS
|
|
46
|
+
SELECT e.uid, min(e.t) AS first_t, max(e.t) AS last_t,
|
|
47
|
+
count(*) AS total_ev,
|
|
48
|
+
count(*) FILTER (WHERE event = 'property viewed') AS views,
|
|
49
|
+
count(*) FILTER (WHERE event = 'offer submitted') AS offers,
|
|
50
|
+
count(*) FILTER (WHERE event = 'property listed') AS listings,
|
|
51
|
+
count(*) FILTER (WHERE event = 'property sold') AS solds,
|
|
52
|
+
count(*) FILTER (WHERE event = 'virtual tour') AS vtours,
|
|
53
|
+
count(*) FILTER (WHERE event = 'in-person tour') AS iptours,
|
|
54
|
+
count(*) FILTER (WHERE event = 'mortgage pre-approval') AS preapps
|
|
55
|
+
FROM ev e GROUP BY 1;
|
|
56
|
+
|
|
57
|
+
CREATE OR REPLACE VIEW puu AS
|
|
58
|
+
SELECT p.*, u.agent_tier, u.user_type, u.pre_approval_status,
|
|
59
|
+
EXISTS (SELECT 1 FROM ev s WHERE s.uid = p.uid AND s.event = 'saved search created'
|
|
60
|
+
AND s.t < p.first_t + INTERVAL '7 days') AS saver,
|
|
61
|
+
EXISTS (SELECT 1 FROM ev v WHERE v.uid = p.uid AND v.event = 'property viewed'
|
|
62
|
+
AND v.t < p.first_t + INTERVAL '14 days') AS viewed14,
|
|
63
|
+
EXISTS (SELECT 1 FROM ev sv WHERE sv.uid = p.uid AND sv.event = 'property saved'
|
|
64
|
+
AND sv.t < p.first_t + INTERVAL '14 days') AS saved14,
|
|
65
|
+
(p.vtours >= 1 AND p.iptours >= 1) AS dual_tour,
|
|
66
|
+
(p.preapps >= 1) AS preapproved
|
|
67
|
+
FROM pu p JOIN users u ON p.uid = u.distinct_id::VARCHAR;
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
-- ── H1-spring-season: offer_price ×2.5 + tour duration ×3, days 30-60 ──
|
|
71
|
+
SELECT 'H1 offer price spring vs outside (expect ~2.6x)' AS q;
|
|
72
|
+
SELECT CASE WHEN day_idx BETWEEN 30 AND 59 THEN 'spring' ELSE 'outside' END AS zone,
|
|
73
|
+
count(*) AS n, round(avg(offer_price), 0) AS avg_price, round(median(offer_price), 0) AS med_price
|
|
74
|
+
FROM ev WHERE event = 'offer submitted' GROUP BY 1;
|
|
75
|
+
|
|
76
|
+
SELECT 'H1 tour duration spring vs outside (expect ~2.65x — H3 clone leak dilutes the 3x knob)' AS q;
|
|
77
|
+
SELECT CASE WHEN day_idx BETWEEN 30 AND 59 THEN 'spring' ELSE 'outside' END AS zone,
|
|
78
|
+
count(*) AS n, round(avg(duration_mins), 2) AS avg_dur
|
|
79
|
+
FROM ev WHERE event = 'tour scheduled' GROUP BY 1;
|
|
80
|
+
|
|
81
|
+
-- ── H2-rate-shock: mortgage_rate pinned 7.5 days 75-89; offers -45% post-75 ──
|
|
82
|
+
SELECT 'H2 rate pin (shock min=max=7.5; outside ~6.35)' AS q;
|
|
83
|
+
SELECT CASE WHEN day_idx BETWEEN 75 AND 88 THEN 'shock' ELSE 'outside' END AS zone,
|
|
84
|
+
count(*) AS n, round(avg(mortgage_rate), 4) AS avg_rate, min(mortgage_rate) AS mn, max(mortgage_rate) AS mx
|
|
85
|
+
FROM ev WHERE event = 'mortgage pre-approval' GROUP BY 1;
|
|
86
|
+
|
|
87
|
+
SELECT 'H2 offer share-of-volume pre vs post d75 (expect ratio ~0.77 = 0.55 knob x clone drift)' AS q;
|
|
88
|
+
SELECT (day_idx > 75) AS post,
|
|
89
|
+
count(*) FILTER (WHERE event = 'offer submitted') AS offers, count(*) AS all_ev,
|
|
90
|
+
round(count(*) FILTER (WHERE event = 'offer submitted')::DOUBLE / count(*), 5) AS offer_share
|
|
91
|
+
FROM ev GROUP BY 1 ORDER BY 1;
|
|
92
|
+
|
|
93
|
+
-- ── H3-saved-search-retention: savers cloned forward, non-savers cut post-d30 ──
|
|
94
|
+
SELECT 'H3 events/user post-day-30 by saver (expect ~6-7x)' AS q;
|
|
95
|
+
SELECT saver, count(*) AS n_users,
|
|
96
|
+
round(avg((SELECT count(*) FROM ev e WHERE e.uid = puu.uid AND e.day_idx > 30)), 2) AS ev_post30_pu
|
|
97
|
+
FROM puu GROUP BY 1;
|
|
98
|
+
|
|
99
|
+
SELECT 'H3 active-in-April, born pre-March (expect savers ~0.98 vs non ~0.55)' AS q;
|
|
100
|
+
SELECT saver, count(*) AS n_users, round(avg((last_t >= TIMESTAMP '2026-04-01')::INT), 4) AS active_apr
|
|
101
|
+
FROM puu WHERE first_t < TIMESTAMP '2026-03-01' GROUP BY 1;
|
|
102
|
+
|
|
103
|
+
-- ── H4-preapproval-conversion: 4-6 offer clones for pre-approved users ──
|
|
104
|
+
SELECT 'H4 offers/user by pre-approval event cohort (expect ~4.5x)' AS q;
|
|
105
|
+
SELECT preapproved, count(*) AS n_users, round(avg(offers), 3) AS offers_pu
|
|
106
|
+
FROM puu GROUP BY 1;
|
|
107
|
+
|
|
108
|
+
SELECT 'H4 profile flag superset check (expect flagged = 1.0 among event cohort)' AS q;
|
|
109
|
+
SELECT round(avg((pre_approval_status = 'approved')::INT), 4) AS flagged, count(*) AS n
|
|
110
|
+
FROM puu WHERE preapproved;
|
|
111
|
+
|
|
112
|
+
-- ── H5-premier-agents: 3x listings / 2x sales for Premier tier ──
|
|
113
|
+
SELECT 'H5 listings + sales per user by agent_tier (expect ~2.8x / ~1.9x)' AS q;
|
|
114
|
+
SELECT agent_tier, count(*) AS n_users,
|
|
115
|
+
round(avg(listings), 4) AS listings_pu, round(avg(solds), 4) AS solds_pu
|
|
116
|
+
FROM puu GROUP BY 1;
|
|
117
|
+
|
|
118
|
+
-- ── H6-dual-tour-buyers: 5-7 offer clones for virtual+in-person tour users ──
|
|
119
|
+
SELECT 'H6 offers/user by dual-tour cohort (expect ~5x)' AS q;
|
|
120
|
+
SELECT dual_tour, count(*) AS n_users, round(avg(offers), 3) AS offers_pu
|
|
121
|
+
FROM puu GROUP BY 1;
|
|
122
|
+
|
|
123
|
+
-- exclusion by PROFILE flag (exact hook-time cohort): the event-cohort
|
|
124
|
+
-- proxy under-excludes users whose pre-approval event H8 later deleted
|
|
125
|
+
-- but who kept their H4 clones, contaminating the baseline (~3.5x)
|
|
126
|
+
SELECT 'H6 overlap decomposition: dual-only vs neither, profile-flag pre-approved excluded (expect ~4.8x)' AS q;
|
|
127
|
+
SELECT dual_tour, count(*) AS n_users, round(avg(offers), 3) AS offers_pu
|
|
128
|
+
FROM puu WHERE pre_approval_status != 'approved' GROUP BY 1;
|
|
129
|
+
|
|
130
|
+
-- ── H7-luxury-release: $2M+ listings only after day 50; luxury-browser cohort ──
|
|
131
|
+
SELECT 'H7 luxury listings by era (expect pre-d50 lux = 0; post share ~1.9% = 3% x 0.643 organic)' AS q;
|
|
132
|
+
SELECT (day_idx >= 50) AS post50, count(*) AS n_listings,
|
|
133
|
+
count(*) FILTER (WHERE listing_price >= 2000000) AS lux_n,
|
|
134
|
+
round(count(*) FILTER (WHERE listing_price >= 2000000)::DOUBLE / count(*), 4) AS lux_share
|
|
135
|
+
FROM ev WHERE event = 'property listed' GROUP BY 1 ORDER BY 1;
|
|
136
|
+
|
|
137
|
+
SELECT 'H7 luxury ($5M+) views by browser cohort (uuid first char = c, 1/16 of users; non-browsers expect 0)' AS q;
|
|
138
|
+
SELECT (left(puu.uid, 1) = 'c') AS browser, count(*) AS n_users,
|
|
139
|
+
round(avg((SELECT count(*) FROM ev e WHERE e.uid = puu.uid AND e.event = 'property viewed' AND e.listing_price >= 5000000)), 3) AS luxviews_pu
|
|
140
|
+
FROM puu GROUP BY 1;
|
|
141
|
+
|
|
142
|
+
-- ── H8-cold-lead-churn: viewed-but-never-saved in first 14d lose 90% after ──
|
|
143
|
+
SELECT 'H8 events/user after first-14d, cold vs rest (expect ~0.13 absolute)' AS q;
|
|
144
|
+
SELECT (viewed14 AND NOT saved14) AS cold, count(*) AS n_users,
|
|
145
|
+
round(avg((SELECT count(*) FROM ev e WHERE e.uid = puu.uid AND e.t > puu.first_t + INTERVAL '14 days')), 2) AS ev_post14_pu,
|
|
146
|
+
round(avg((SELECT count(*) FROM ev e WHERE e.uid = puu.uid AND e.t <= puu.first_t + INTERVAL '14 days')), 2) AS ev_pre14_pu
|
|
147
|
+
FROM puu WHERE first_t < TIMESTAMP '2026-04-01' GROUP BY 1;
|
|
148
|
+
|
|
149
|
+
-- ── H9-view-magic-number: 6-12 views => +30% offer_price; 13+ => fewer offers ──
|
|
150
|
+
SELECT 'H9 offer price by view bucket, non-spring (expect sweet/low ~1.35; over/low placebo ~1.0)' AS q;
|
|
151
|
+
SELECT CASE WHEN p.views BETWEEN 6 AND 12 THEN 'sweet' WHEN p.views < 6 THEN 'low' ELSE 'over' END AS bucket,
|
|
152
|
+
count(*) AS n_offers, count(DISTINCT e.uid) AS n_users, round(avg(e.offer_price), 0) AS avg_price
|
|
153
|
+
FROM puu p JOIN ev e ON e.uid = p.uid AND e.event = 'offer submitted'
|
|
154
|
+
WHERE e.day_idx NOT BETWEEN 30 AND 59
|
|
155
|
+
GROUP BY 1 ORDER BY 1;
|
|
156
|
+
|
|
157
|
+
SELECT 'H9 offers/user over vs sweet (expect ~0.65 — 60% knob nets the visible ~35% drop)' AS q;
|
|
158
|
+
SELECT CASE WHEN views BETWEEN 6 AND 12 THEN 'sweet' WHEN views >= 13 THEN 'over' ELSE 'low' END AS bucket,
|
|
159
|
+
count(*) AS n_users, round(avg(offers), 3) AS offers_pu
|
|
160
|
+
FROM puu GROUP BY 1 ORDER BY 1;
|
|
161
|
+
|
|
162
|
+
-- ── H10-tour-ttc-by-tier: emulator-only (see stories) — identity invariants here ──
|
|
163
|
+
SELECT 'H10 identity invariants (expect uid_resolved = 1.0, stamp_agree = 1.0)' AS q;
|
|
164
|
+
SELECT count(*) AS n,
|
|
165
|
+
round(avg((u.distinct_id IS NOT NULL)::INT), 6) AS uid_resolved,
|
|
166
|
+
round(avg(CASE WHEN u.distinct_id IS NOT NULL THEN (e.user_type = u.user_type)::INT END), 6) AS stamp_agree
|
|
167
|
+
FROM ev e LEFT JOIN users u ON e.uid = u.distinct_id::VARCHAR;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* real-estate.verify.mjs — thin wrapper around the story runner.
|
|
4
|
+
*
|
|
5
|
+
* All verification logic lives in the `stories` export of ./real-estate.js;
|
|
6
|
+
* this script just streams the shards and delegates. It is equivalent to:
|
|
7
|
+
*
|
|
8
|
+
* node scripts/verify-stories.mjs dungeons/vertical/real-estate/real-estate.js --data-prefix verify-real-estate
|
|
9
|
+
*
|
|
10
|
+
* Generate first:
|
|
11
|
+
* node scripts/verify-runner.mjs dungeons/vertical/real-estate/real-estate.js verify-real-estate
|
|
12
|
+
* Run:
|
|
13
|
+
* node dungeons/vertical/real-estate/real-estate.verify.mjs [prefix] # default verify-real-estate
|
|
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 './real-estate.js';
|
|
22
|
+
|
|
23
|
+
const PREFIX = process.argv[2] || 'verify-real-estate';
|
|
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/real-estate/real-estate.js ${PREFIX}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(`real-estate — 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) — H10's emulator stories carry their own
|
|
55
|
+
// conversion window: 31.2h = 24h generative Tour Funnel TTC × the 1.3
|
|
56
|
+
// Standard-tier stretch, so the stretched support is fully covered. The
|
|
57
|
+
// PRIMARY read is the 2-step view→tour-scheduled pair; the doc's 3-step read
|
|
58
|
+
// (…→offer submitted) stays as a directional secondary because H4/H6 offer
|
|
59
|
+
// clones at random timestamps collide with the greedy third-step pick and
|
|
60
|
+
// attenuate the ratio toward 1 (see the H10 story narrative).
|
|
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);
|