@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,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);