@ak--47/dungeon-master 1.5.4 → 1.6.1

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 (149) 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/powertools/SKILL.md +75 -0
  6. package/.claude/skills/powertools/pt.mjs +91 -0
  7. package/.claude/skills/powertools/snapshot-project.mjs +124 -0
  8. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  9. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  10. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  11. package/.claude/skills/write-hooks/SKILL.md +156 -18
  12. package/CHANGELOG.md +224 -0
  13. package/HOOKS.md +566 -112
  14. package/README.md +30 -9
  15. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  16. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  17. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  18. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  19. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  20. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  21. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  22. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  23. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  24. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  25. package/dungeons/technical/stories-verify.js +152 -0
  26. package/dungeons/vertical/README.md +154 -0
  27. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  28. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  29. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  30. package/dungeons/vertical/community/community.js +1589 -0
  31. package/dungeons/vertical/community/community.sql +216 -0
  32. package/dungeons/vertical/community/community.verify.mjs +72 -0
  33. package/dungeons/vertical/crypto/crypto.js +1483 -0
  34. package/dungeons/vertical/crypto/crypto.sql +194 -0
  35. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  36. package/dungeons/vertical/dating/dating.js +1540 -0
  37. package/dungeons/vertical/dating/dating.sql +288 -0
  38. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  39. package/dungeons/vertical/devtools/devtools.js +1524 -0
  40. package/dungeons/vertical/devtools/devtools.sql +161 -0
  41. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  42. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  43. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  44. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  45. package/dungeons/vertical/education/education.js +1763 -0
  46. package/dungeons/vertical/education/education.sql +243 -0
  47. package/dungeons/vertical/education/education.verify.mjs +75 -0
  48. package/dungeons/vertical/fintech/fintech.js +1410 -0
  49. package/dungeons/vertical/fintech/fintech.sql +175 -0
  50. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  51. package/dungeons/vertical/fitness/fitness.js +1495 -0
  52. package/dungeons/vertical/fitness/fitness.sql +206 -0
  53. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  54. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  55. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  56. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  57. package/dungeons/vertical/gaming/gaming.js +1741 -0
  58. package/dungeons/vertical/gaming/gaming.sql +249 -0
  59. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  60. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  61. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  62. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  63. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  64. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  65. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  66. package/dungeons/vertical/logistics/logistics.js +1541 -0
  67. package/dungeons/vertical/logistics/logistics.sql +205 -0
  68. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  69. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  70. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  71. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  72. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  73. package/dungeons/vertical/media/media.sql +173 -0
  74. package/dungeons/vertical/media/media.verify.mjs +74 -0
  75. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  76. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  77. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  78. package/dungeons/vertical/sass/sass.js +1733 -0
  79. package/dungeons/vertical/sass/sass.sql +173 -0
  80. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  81. package/dungeons/vertical/social/social.js +1456 -0
  82. package/dungeons/vertical/social/social.sql +160 -0
  83. package/dungeons/vertical/social/social.verify.mjs +76 -0
  84. package/dungeons/vertical/streaming/streaming.js +677 -0
  85. package/dungeons/vertical/streaming/streaming.sql +101 -0
  86. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  87. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  88. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  89. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  90. package/dungeons/vertical/travel/travel.js +1603 -0
  91. package/dungeons/vertical/travel/travel.sql +236 -0
  92. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  93. package/index.js +7 -3
  94. package/lib/core/config-validator.js +37 -2
  95. package/lib/core/context.js +4 -2
  96. package/lib/core/dungeon-loader.js +20 -15
  97. package/lib/core/dungeon-to-json.js +22 -4
  98. package/lib/core/storage.js +3 -3
  99. package/lib/generators/funnels.js +36 -9
  100. package/lib/hook-helpers/cohort.js +53 -0
  101. package/lib/hook-helpers/index.js +8 -0
  102. package/lib/hook-helpers/mutate.js +1 -1
  103. package/lib/hook-helpers/shape.js +319 -0
  104. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  105. package/lib/hook-patterns/attributed-by-source.js +60 -58
  106. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  107. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  108. package/lib/hook-patterns/index.js +1 -1
  109. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  110. package/lib/orchestrators/user-loop.js +41 -0
  111. package/lib/templates/story-spec.schema.json +175 -0
  112. package/lib/utils/json-evaluator.js +1 -1
  113. package/lib/utils/utils.js +151 -59
  114. package/lib/verify/coerce.js +186 -0
  115. package/lib/verify/counting.js +270 -32
  116. package/lib/verify/emulate-breakdown.js +1285 -203
  117. package/lib/verify/first-time.js +89 -0
  118. package/lib/verify/flows.js +679 -0
  119. package/lib/verify/formula.js +259 -0
  120. package/lib/verify/funnel-engine.js +697 -113
  121. package/lib/verify/index.js +32 -4
  122. package/lib/verify/sessionize.js +275 -0
  123. package/lib/verify/story-runner.js +483 -0
  124. package/lib/verify/verify-dungeon.js +59 -37
  125. package/package.json +2 -1
  126. package/scripts/extract-dungeon-schema.mjs +2 -2
  127. package/scripts/run-many.mjs +2 -2
  128. package/scripts/verify-runner.mjs +2 -2
  129. package/scripts/verify-stories.mjs +244 -0
  130. package/types.d.ts +298 -30
  131. package/dungeons/vertical/ai-platform.js +0 -818
  132. package/dungeons/vertical/community.js +0 -966
  133. package/dungeons/vertical/crypto.js +0 -895
  134. package/dungeons/vertical/dating.js +0 -830
  135. package/dungeons/vertical/devtools.js +0 -1050
  136. package/dungeons/vertical/ecommerce.js +0 -811
  137. package/dungeons/vertical/education.js +0 -964
  138. package/dungeons/vertical/fintech.js +0 -946
  139. package/dungeons/vertical/fitness.js +0 -935
  140. package/dungeons/vertical/food-delivery.js +0 -934
  141. package/dungeons/vertical/gaming.js +0 -1120
  142. package/dungeons/vertical/healthcare.js +0 -945
  143. package/dungeons/vertical/insurance-application.js +0 -1039
  144. package/dungeons/vertical/logistics.js +0 -986
  145. package/dungeons/vertical/marketplace.js +0 -1019
  146. package/dungeons/vertical/real-estate.js +0 -872
  147. package/dungeons/vertical/sass.js +0 -995
  148. package/dungeons/vertical/social.js +0 -904
  149. package/dungeons/vertical/travel.js +0 -930
@@ -0,0 +1,205 @@
1
+ -- ============================================================
2
+ -- logistics.js — v1.6 human-inspection queries (DuckDB)
3
+ --
4
+ -- Every query is keyed to a story id in logistics.js's `stories` export;
5
+ -- the machine-checked verdicts come from:
6
+ -- node scripts/verify-stories.mjs dungeons/vertical/logistics/logistics.js --data-prefix verify-logistics
7
+ -- Generate first:
8
+ -- node scripts/verify-runner.mjs dungeons/vertical/logistics/logistics.js verify-logistics
9
+ -- Run this file:
10
+ -- duckdb -c ".read dungeons/vertical/logistics/logistics.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-logistics-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-logistics-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 counts. Inventory checks are ONE-SIDED (only H6 deletes them,
34
+ -- for trial users, and H9 reads them AFTER H6 ran) — output counts equal
35
+ -- H9's hook-time counts exactly. POs are deleted only for 16+ checkers
36
+ -- (H9); stockout alerts only for enterprise (H3); clones are identified
37
+ -- by report_type = 'integration_summary' (outside the organic pool).
38
+ CREATE OR REPLACE VIEW per_user AS
39
+ SELECT uid,
40
+ count(*) AS total,
41
+ count(*) FILTER (WHERE event = 'inventory checked') AS inv,
42
+ count(*) FILTER (WHERE event = 'purchase order created') AS po,
43
+ count(*) FILTER (WHERE event = 'stockout alert') AS so,
44
+ count(*) FILTER (WHERE event = 'integration connected') AS ic,
45
+ count(*) FILTER (WHERE event = 'report generated' AND report_type = 'integration_summary') AS clones,
46
+ min(t) AS first_t
47
+ FROM ev GROUP BY 1;
48
+
49
+
50
+ -- ── H1-month-end-pages ──────────────────────────────────────
51
+ -- Reports on calendar days >= 28 get report_pages ×2.5 (floored). H4's
52
+ -- clones (report_type 'integration_summary', uniform [5,25] pages stamped
53
+ -- AFTER H1) dilute a pooled read — exclude them; they are the placebo arm.
54
+ SELECT (report_type = 'integration_summary') AS is_clone,
55
+ CASE WHEN extract(day FROM t) >= 28 THEN 'month_end' ELSE 'mid_month' END AS bucket,
56
+ count(*) AS n, round(avg(report_pages), 1) AS avg_pages
57
+ FROM ev WHERE event = 'report generated'
58
+ GROUP BY 1, 2 ORDER BY 1, 2;
59
+ -- read: organic month_end/mid_month ≈ 2.4-2.5 (floor loss ~1%);
60
+ -- clone ratio ≈ 1.0 (placebo)
61
+
62
+
63
+ -- ── H2-rush-order-premium ───────────────────────────────────
64
+ -- 'urgent' POs get unit_cost ×1.5 (floored); 'expedited' is untreated.
65
+ SELECT priority, count(*) AS n, round(avg(unit_cost), 1) AS avg_cost
66
+ FROM ev WHERE event = 'purchase order created'
67
+ GROUP BY 1 ORDER BY 1;
68
+ -- read: urgent/standard ≈ 1.5; expedited/standard ≈ 1.0 (placebo)
69
+
70
+
71
+ -- ── H3-stockout-by-tier ─────────────────────────────────────
72
+ -- Enterprise loses 10% of stockout alerts. Per-user LEVELS are dominated
73
+ -- by persona multipliers (enterprise 5x) — read the stockout-per-
74
+ -- inventory-check ratio instead; the supply-chain worldEvent (×3, days
75
+ -- 35-40) hits all tiers alike and cancels cross-tier.
76
+ SELECT u.company_tier AS tier, count(*) AS users,
77
+ round(sum(p.so)::DOUBLE / sum(p.inv), 4) AS so_per_inv
78
+ FROM per_user p JOIN users u ON u.distinct_id::VARCHAR = p.uid
79
+ GROUP BY 1 ORDER BY 1;
80
+ -- read: enterprise/small_business ≈ 0.88; mid_market/small_business ≈ 1.0
81
+
82
+
83
+ -- ── H4-integration-reports ──────────────────────────────────
84
+ -- Users with >= 3 'integration connected' get a cloned 'report generated'
85
+ -- per integration at 65% (+1-5d later, report_type 'integration_summary').
86
+ -- H6 (the only integration-deleter) runs BEFORE H4 → output counts equal
87
+ -- H4's hook-time counts exactly; the treated cohort is fully recoverable.
88
+ SELECT (p.ic >= 3) AS treated, count(*) AS users,
89
+ round(sum(p.clones)::DOUBLE / nullif(sum(p.ic), 0), 4) AS clones_per_integration,
90
+ round(avg((p.clones > 0)::INT), 4) AS any_clone_share
91
+ FROM per_user p GROUP BY 1 ORDER BY 1;
92
+ -- read: treated clones_per_integration ≈ 0.63 (0.65 × ~2% future-guard
93
+ -- loss); untreated any_clone_share = 0 (structural — leakage would
94
+ -- mean the hook-order invariant broke)
95
+
96
+
97
+ -- ── H5-alert-fatigue ────────────────────────────────────────
98
+ -- Users with > 30 stockout alerts: response_time_hours scaled from alert
99
+ -- index 20 on, ×(1.5 + 1.5×(idx-20)/n). Hook index = record order; time
100
+ -- order matches exactly (iteration placebo 1.000). Control = 20-30-alert
101
+ -- users (never treated, same iid response_time pool).
102
+ WITH al AS (
103
+ SELECT uid, response_time_hours AS rt,
104
+ row_number() OVER (PARTITION BY uid ORDER BY t) - 1 AS idx,
105
+ count(*) OVER (PARTITION BY uid) AS n
106
+ FROM ev WHERE event = 'stockout alert'
107
+ )
108
+ SELECT arm, count(DISTINCT uid) AS users, round(avg(rt), 2) AS avg_rt FROM (
109
+ SELECT uid, rt, 'late_treated' AS arm FROM al WHERE n > 30 AND idx >= 25
110
+ UNION ALL
111
+ SELECT uid, rt, 'early_untreated' FROM al WHERE n > 30 AND idx <= 14
112
+ UNION ALL
113
+ SELECT uid, rt, 'control_20_30' FROM al WHERE n BETWEEN 20 AND 30
114
+ ) GROUP BY 1 ORDER BY 1;
115
+ -- read: late_treated/control ≈ 2.0; early_untreated/control ≈ 1.0
116
+
117
+
118
+ -- ── H6-trial-churn ──────────────────────────────────────────
119
+ -- Trial-tier users lose 50% of events after day 7 from first event
120
+ -- (v1.6 behavior change: v1.5 keyed on record.length < 10 — matched ~0.9%
121
+ -- of users, never touched trials). Cross-tier levels are incomparable
122
+ -- (activeWindow 14d, 0.4x multiplier) — read each tier's own
123
+ -- rate(day 8-13)/rate(day 1-6); the ratio cancels the level.
124
+ WITH fe AS (SELECT uid, min(t) AS f FROM ev GROUP BY 1),
125
+ rd AS (SELECT e.uid, date_diff('day', fe.f, e.t) AS d FROM ev e JOIN fe ON fe.uid = e.uid)
126
+ SELECT u.company_tier AS tier, count(DISTINCT r.uid) AS users,
127
+ round(count(*) FILTER (WHERE d BETWEEN 8 AND 13)::DOUBLE
128
+ / count(*) FILTER (WHERE d BETWEEN 1 AND 6), 4) AS wk2_over_wk1
129
+ FROM rd r JOIN users u ON u.distinct_id::VARCHAR = r.uid
130
+ GROUP BY 1 ORDER BY 1;
131
+ -- read: trial ratio ≈ 0.5 × small_business ratio (DiD ≈ 0.55);
132
+ -- mid_market ≈ small_business (placebo)
133
+
134
+
135
+ -- ── H7-enterprise-profiles ──────────────────────────────────
136
+ -- user hook overwrites warehouse_count/employee_count per tier with
137
+ -- disjoint uniform ranges. Personas cover 100% of users → ranges EXACT.
138
+ SELECT company_tier AS tier, count(*) AS users,
139
+ min(warehouse_count) AS min_wh, max(warehouse_count) AS max_wh,
140
+ min(employee_count) AS min_emp, max(employee_count) AS max_emp
141
+ FROM users GROUP BY 1 ORDER BY 1;
142
+ -- read: enterprise wh [5,15] emp [200,2000]; mid_market wh [2,6]
143
+ -- emp [20,200]; small_business wh [1,3] emp [5,80];
144
+ -- trial wh = 1 emp [1,10] — zero out-of-range rows
145
+
146
+
147
+ -- ── H8-smb-conversion-drop ──────────────────────────────────
148
+ -- small_business loses 35% of 'alert configured' — last step of
149
+ -- Integration Setup. CAUTION: cross-event SQL step-pairing here is the
150
+ -- documented greedy-single-pass limitation; the story asserts conversion
151
+ -- through the emulator (timeToConvert, 48h treated / 336h Supplier
152
+ -- Management placebo). This query shows the RAW event-count shadow only.
153
+ SELECT u.company_tier AS tier,
154
+ count(*) FILTER (WHERE e.event = 'report generated') AS step2_events,
155
+ count(*) FILTER (WHERE e.event = 'alert configured') AS step3_events,
156
+ round(count(*) FILTER (WHERE e.event = 'alert configured')::DOUBLE
157
+ / count(*) FILTER (WHERE e.event = 'report generated'), 4) AS ac_per_rg
158
+ FROM ev e JOIN users u ON u.distinct_id::VARCHAR = e.uid
159
+ WHERE u.company_tier IN ('small_business', 'mid_market')
160
+ GROUP BY 1 ORDER BY 1;
161
+ -- read: smb ac_per_rg depressed vs mid_market (raw shadow of the 35% drop;
162
+ -- trust the story's emulator verdict for the funnel-conversion read)
163
+
164
+
165
+ -- ── H9-inventory-magic-number ───────────────────────────────
166
+ -- Sweet 5-15 inventory checks → PO quantity ×1.4; 16+ checks → 60% of POs
167
+ -- dropped. Value read: quantity is an iid pool draw — sweet/low mean ratio
168
+ -- reads the knob; unit_cost is the placebo.
169
+ SELECT CASE WHEN p.inv BETWEEN 5 AND 15 THEN 'sweet' WHEN p.inv <= 4 THEN 'low' END AS arm,
170
+ count(DISTINCT p.uid) AS users,
171
+ round(avg(e.quantity), 1) AS avg_qty, round(avg(e.unit_cost), 1) AS avg_cost
172
+ FROM per_user p JOIN ev e ON e.uid = p.uid AND e.event = 'purchase order created'
173
+ WHERE p.inv <= 15
174
+ GROUP BY 1 ORDER BY 1;
175
+ -- read: sweet/low avg_qty ≈ 1.4; avg_cost ≈ 1.0 (placebo)
176
+
177
+ -- volume read: PO count is activity-coupled → no cross-arm level works.
178
+ -- Read PO-per-inventory-check within small_business only (constant
179
+ -- conversionModifier), treated cliff bin 16-23 vs adjacent untreated
180
+ -- 12-15, with a flatness guard on the pre-cliff bins.
181
+ SELECT CASE WHEN p.inv BETWEEN 4 AND 7 THEN 'b04_07'
182
+ WHEN p.inv BETWEEN 8 AND 11 THEN 'b08_11'
183
+ WHEN p.inv BETWEEN 12 AND 15 THEN 'b12_15'
184
+ WHEN p.inv BETWEEN 16 AND 23 THEN 'b16_23'
185
+ WHEN p.inv BETWEEN 24 AND 40 THEN 'b24_40' END AS bin,
186
+ count(*) AS users, round(sum(p.po)::DOUBLE / sum(p.inv), 4) AS po_per_inv
187
+ FROM per_user p JOIN users u ON u.distinct_id::VARCHAR = p.uid
188
+ WHERE u.company_tier = 'small_business' AND p.inv BETWEEN 4 AND 40
189
+ GROUP BY 1 ORDER BY 1;
190
+ -- read: pre-cliff bins flat (~1.6, mild organic decline), then
191
+ -- b16_23/b12_15 ≈ 0.4 × organic gradient [0.83, 1.0] ≈ 0.37
192
+
193
+
194
+ -- ── H10-onboarding-ttc ──────────────────────────────────────
195
+ -- funnel-post scales Onboarding inter-step gaps: enterprise ×0.71,
196
+ -- small_business/trial ×1.3, mid_market untouched (v1.6 scopes the hook
197
+ -- to Onboarding only). CAUTION: cross-event TTC SQL is the documented
198
+ -- greedy-single-pass limitation — it pairs steps across funnel instances
199
+ -- and buries the signal. The story asserts TTC through the emulator
200
+ -- (timeToConvert, 93.6h window = 72h generative × 1.3 max stretch);
201
+ -- trust the story verdict, not ad-hoc pair SQL. Only born-in-dataset
202
+ -- users (~12%) have 'account created' in-window — cohort sanity below.
203
+ SELECT u.company_tier AS tier, count(*) AS account_created_events
204
+ FROM ev e JOIN users u ON u.distinct_id::VARCHAR = e.uid
205
+ WHERE e.event = 'account created' GROUP BY 1 ORDER BY 1;
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * logistics.verify.mjs — thin wrapper around the story runner.
4
+ *
5
+ * All verification logic lives in the `stories` export of ./logistics.js;
6
+ * this script just streams the shards and delegates. It is equivalent to:
7
+ *
8
+ * node scripts/verify-stories.mjs dungeons/vertical/logistics/logistics.js --data-prefix verify-logistics
9
+ *
10
+ * Generate first:
11
+ * node scripts/verify-runner.mjs dungeons/vertical/logistics/logistics.js verify-logistics
12
+ * Run:
13
+ * node dungeons/vertical/logistics/logistics.verify.mjs [prefix] # default verify-logistics
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 './logistics.js';
22
+
23
+ const PREFIX = process.argv[2] || 'verify-logistics';
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/logistics/logistics.js ${PREFIX}`);
44
+ process.exit(1);
45
+ }
46
+ console.log(`logistics — 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 emulator stories carry their own
55
+ // explicit conversion windows (H8: 48h Integration Setup treated arm, 336h
56
+ // Supplier Management placebo; H10: 93.6h = 72h Onboarding generative
57
+ // window × the 1.3 small-business stretch, covering the stretched support),
58
+ // so no funnel-default threading is needed
59
+ const results = await evaluateStories(stories, events, {
60
+ profiles,
61
+ funnels: config.funnels,
62
+ identityMap: buildIdentityMap(profiles),
63
+ runSql,
64
+ });
65
+
66
+ let worst = 'NAILED';
67
+ for (const r of results) {
68
+ console.log(`${r.verdict.padEnd(7)} ${r.id}`);
69
+ for (const a of r.assertions) console.log(` ${a.verdict.padEnd(7)} ${a.detail}`);
70
+ if (VERDICT_RANK[r.verdict] < VERDICT_RANK[worst]) worst = r.verdict;
71
+ }
72
+ console.log(`\nworst verdict: ${worst}`);
73
+ process.exit(VERDICT_RANK[worst] >= VERDICT_RANK.STRONG ? 0 : 1);