@ak--47/dungeon-master 1.4.5 → 1.5.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 (65) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +139 -0
  9. package/HOOKS.md +1243 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +1 -1
  12. package/dungeons/technical/anonymous-users.js +1 -1
  13. package/dungeons/technical/array-of-object-lookup.js +1 -1
  14. package/dungeons/technical/datagen-v15-verify.js +74 -0
  15. package/dungeons/technical/experiments.js +1 -1
  16. package/dungeons/technical/foobar.js +1 -1
  17. package/dungeons/technical/group-analytics.js +1 -1
  18. package/dungeons/technical/mirror-strategies.js +1 -1
  19. package/dungeons/technical/nested-objects.js +1 -1
  20. package/dungeons/technical/retention-cadence.js +1 -1
  21. package/dungeons/technical/sanity.js +1 -1
  22. package/dungeons/technical/scale-test.js +1 -1
  23. package/dungeons/technical/scd.js +1 -1
  24. package/dungeons/technical/simple.js +1 -1
  25. package/dungeons/technical/simplest.js +74 -20
  26. package/dungeons/technical/text-generation.js +1 -1
  27. package/dungeons/vertical/ai-platform.js +4 -0
  28. package/dungeons/vertical/community.js +9 -3
  29. package/dungeons/vertical/crypto.js +5 -0
  30. package/dungeons/vertical/dating.js +23 -10
  31. package/dungeons/vertical/devtools.js +10 -0
  32. package/dungeons/vertical/ecommerce.js +6 -0
  33. package/dungeons/vertical/education.js +11 -0
  34. package/dungeons/vertical/fintech.js +13 -0
  35. package/dungeons/vertical/fitness.js +10 -0
  36. package/dungeons/vertical/food-delivery.js +9 -0
  37. package/dungeons/vertical/gaming.js +10 -0
  38. package/dungeons/vertical/healthcare.js +5 -0
  39. package/dungeons/vertical/insurance-application.js +10 -0
  40. package/dungeons/vertical/logistics.js +8 -1
  41. package/dungeons/vertical/marketplace.js +7 -0
  42. package/dungeons/vertical/media.js +8 -0
  43. package/dungeons/vertical/real-estate.js +7 -1
  44. package/dungeons/vertical/sass.js +12 -0
  45. package/dungeons/vertical/social.js +9 -0
  46. package/dungeons/vertical/travel.js +5 -0
  47. package/index.js +19 -4
  48. package/lib/core/config-validator.js +270 -7
  49. package/lib/core/dungeon-loader.js +2 -5
  50. package/lib/generators/events.js +12 -13
  51. package/lib/generators/funnels.js +72 -1
  52. package/lib/hook-helpers/index.js +1 -0
  53. package/lib/hook-helpers/inject.js +95 -0
  54. package/lib/orchestrators/user-loop.js +478 -29
  55. package/lib/templates/macro-presets.js +39 -9
  56. package/lib/utils/utils.js +16 -79
  57. package/lib/verify/counting.js +320 -0
  58. package/lib/verify/emulate-breakdown.js +512 -108
  59. package/lib/verify/funnel-engine.js +539 -0
  60. package/lib/verify/identity.js +78 -0
  61. package/lib/verify/index.js +19 -0
  62. package/lib/verify/verify-dungeon.js +58 -0
  63. package/package.json +4 -2
  64. package/types.d.ts +237 -4
  65. package/scripts/smoke-test-all.mjs +0 -162
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: verify-dungeon
3
+ description: Use when a dungeon's hooks need verification before pushing data to Mixpanel — runs the dungeon at full scale, validates schema integrity (catches flag-stamping), queries with DuckDB using Mixpanel-accurate counting semantics, and reports per-hook NAILED/STRONG/WEAK/NONE/INVERSE verdicts.
4
+ argument-hint: [dungeon path(s), e.g. dungeons/gaming.js or dungeons/fintech.js]
5
+ model: claude-opus-4-6
6
+ effort: max
7
+ ---
8
+
9
+ # Verify Dungeon
10
+
11
+ Verify a dungeon at full scale: validate schema integrity, query the output with the Mixpanel emulator (preferred) or DuckDB, write a single consolidated `hook-results.md`.
12
+
13
+ **Dungeon file(s):** `$ARGUMENTS` — single path, multiple space-separated paths, or glob pattern. In batch mode, process each dungeon sequentially through Steps 1–3, then write one consolidated report in Step 4. Use a unique `name` prefix per dungeon (e.g., `verify-fintech`, `verify-gaming`) so output files don't collide.
14
+
15
+ ## Reference files
16
+
17
+ Load these on demand:
18
+
19
+ - [references/counting-semantics.md](references/counting-semantics.md) — Mixpanel counting rules, when to use the emulator vs DuckDB, emulator analysis types, identity-model verification, time-series, common gotchas.
20
+ - [references/sql-recipes.md](references/sql-recipes.md) — every DuckDB query template (schema validation, identity / experiment invariants, hook archetype queries, pitfalls, TTC verification, dataset-window computation).
21
+ - [references/report-format.md](references/report-format.md) — single + multi-dungeon report templates, per-hook detail block, verdict criteria, query log format, mandatory verification SQL file for user dungeons.
22
+
23
+ Also: `HOOKS.md` (recipe encyclopedia) and `types.d.ts` (source of truth for hook meta interfaces).
24
+
25
+ ## Pipeline
26
+
27
+ ### Step 1: Read & catalog the hooks
28
+
29
+ Read the dungeon at `$ARGUMENTS`. If it's a bare filename (no `/`), check `dungeons/`. Find:
30
+
31
+ 1. **The `hook:` function** — read the full body
32
+ 2. **The documentation comment block** describing engineered patterns
33
+ 3. **Module-level closure state** (Map / Set / tracking variables defined outside the hook function but used inside)
34
+
35
+ For each hook/pattern, catalog:
36
+ - Hook number and name (e.g., "Hook #1: Compass users have 3x quest completion")
37
+ - Hook type (`event`, `everything`, `funnel-pre`, `funnel-post`, `user`, `scd-pre`)
38
+ - Mechanism — what the code actually does
39
+ - Expected signal — specific, measurable outcome (e.g., "compass_user=true events should have ~1.5x reward_gold")
40
+ - Which output file the signal lives in (events, users, groups)
41
+ - Mixpanel report instructions — flag missing/vague ones for the report
42
+
43
+ ### Step 2: Run the dungeon
44
+
45
+ The runner already exists at `scripts/verify-runner.mjs`. Use it — do NOT recreate.
46
+
47
+ **ALWAYS run at full fidelity. Never use `--small` for verification.** `--small` runs (1K users, 100K events) compress per-cohort populations and shift ratios within ±25%, hiding real bugs and flagging fake ones. They exist in the runner only as a developer-troubleshooting escape hatch.
48
+
49
+ ```bash
50
+ node scripts/verify-runner.mjs <dungeon-path> <run-name>
51
+ ```
52
+
53
+ Example:
54
+ ```bash
55
+ node scripts/verify-runner.mjs dungeons/vertical/gaming.js verify-gaming
56
+ ```
57
+
58
+ Full-fidelity runs can take minutes (50K+ user dungeons). Plan accordingly — kick off the run, do other reading, return when the file lands. If a run takes longer than your budget, report that as a finding ("dungeon too large to verify in current session") rather than falling back to `--small`.
59
+
60
+ **Expected output files** (in `./data/`, prefixed by `<run-name>`):
61
+ - `<run-name>-EVENTS.json` — all events (JSONL)
62
+ - `<run-name>-USERS.json` — user profiles
63
+ - `<run-name>-*-GROUPS.json` — group profiles (if dungeon has groups)
64
+ - `<run-name>-*-SCD.json` — SCD data (if dungeon has SCDs)
65
+
66
+ For batched output (>2M events), the runner writes `<run-name>-EVENTS-part-*.json`. See [sql-recipes.md "Multi-part EVENTS files"](references/sql-recipes.md#multi-part-events-files-batch-mode).
67
+
68
+ ### Step 3: Validate schema (BEFORE per-hook checks)
69
+
70
+ Catches hooks that introduce undeclared columns (flag stamping). For each unique event type, compare actual columns against config-declared properties. See [sql-recipes.md "Schema validation queries"](references/sql-recipes.md#schema-validation-queries) for the SQL and the expected-schema source table.
71
+
72
+ **Schema verdicts:**
73
+ - **SCHEMA-PASS** — added column appears on 100% of events of this type (uniform enrichment, acceptable)
74
+ - **SCHEMA-FAIL** — added column appears on <100% (flag stamping; conditional property creates inconsistent schema)
75
+
76
+ If any event type has SCHEMA-FAIL, flag it prominently in the report header with specific remediation: which hook line adds the property and how to remove it while preserving the intended pattern.
77
+
78
+ ### Step 4: Verify each hook
79
+
80
+ **Decision: emulator vs DuckDB**
81
+
82
+ | Pattern | Use |
83
+ |---|---|
84
+ | Funnel completion / step conversion | `emulateBreakdown({type: 'funnelFrequency'})` |
85
+ | Insights frequency-distribution | `emulateBreakdown({type: 'frequencyByFrequency'})` |
86
+ | Avg(prop) by per-user count(B) | `emulateBreakdown({type: 'aggregatePerUser'})` |
87
+ | Funnel TTC by segment | `emulateBreakdown({type: 'timeToConvert'})` |
88
+ | First/last touch attribution | `emulateBreakdown({type: 'attributedBy'})` |
89
+ | Retention curves | `emulateBreakdown({type: 'retention'})` |
90
+ | Per-session metrics | `emulateBreakdown({type: 'sessionMetrics'})` |
91
+ | Schema integrity / column coverage | DuckDB |
92
+ | Identity-model invariants | DuckDB |
93
+ | Experiment invariants | DuckDB |
94
+ | Bespoke time-window patterns | DuckDB |
95
+
96
+ **Hand-written DuckDB funnel SQL diverges from Mixpanel — never hand-roll.** If you find yourself writing `WITH step1 AS ..., step2 AS ...` for a funnel, STOP — use `emulateBreakdown` with `funnelFrequency` instead.
97
+
98
+ For emulator details, identity-model dungeons (must pass `profiles`), and time-series breakdown via `timeBucket`, see [counting-semantics.md](references/counting-semantics.md). For DuckDB query templates by hook archetype, pitfalls, and standard checks, see [sql-recipes.md](references/sql-recipes.md).
99
+
100
+ **Always run for every dungeon** (before per-hook checks):
101
+ - Standard identity-model invariants (stitch counts, pre-existing user stamping) when the dungeon uses the identity model
102
+ - Experiment invariants (variant distribution, exposure timing, deterministic assignment) when any funnel uses `experiment:`
103
+ - SuperProp consistency, SuperProp/UserProp mirror, Mixpanel default-property casing, funnel-pre dilution
104
+
105
+ ### Step 5: Stash query log (optional)
106
+
107
+ If `./research/` exists locally, write every DuckDB query execution to `./research/hook-query-log.txt`. Format and conventions: see [report-format.md "Query log format"](references/report-format.md#query-log-format). If `./research/` does not exist, skip — do not create the directory.
108
+
109
+ ### Step 6: Write `./research/hook-results.md`
110
+
111
+ Use the templates in [report-format.md](references/report-format.md):
112
+ - Single-dungeon report structure
113
+ - Multi-dungeon report structure (when batch mode)
114
+ - Per-hook detail block
115
+ - Verdict criteria (5-tier)
116
+
117
+ **Order failures first** within each dungeon section: INVERSE → NONE → WEAK → STRONG → NAILED. Sort the summary table the same way. Actionable issues at the top.
118
+
119
+ ### Step 6b: Write verification SQL (mandatory for user dungeons)
120
+
121
+ When verifying a dungeon in `dungeons/user/`, also write a standalone DuckDB SQL file at `dungeons/user/<name>-verifications.sql`. Vertical dungeons already have their SQL in `verification/verticals/`. Format: see [report-format.md "Verification SQL file"](references/report-format.md#verification-sql-file-mandatory-for-user-dungeons).
122
+
123
+ ### Step 7: Cleanup
124
+
125
+ ```bash
126
+ rm -f ./data/verify-* ./verify-*
127
+ ```
128
+
129
+ Remove ALL files matching `verify-*` in `./data/` and project root. Also remove any temporary runner scripts.
130
+
131
+ ## Hook execution model
132
+
133
+ Per user, hooks fire in this order:
134
+
135
+ 1. `"user"` — profile created (mutate in-place; return ignored)
136
+ 2. `"scd-pre"` — SCD entries created (mutate in-place OR return new array)
137
+ 3. For each funnel: `"funnel-pre"` → `"event"` (per step) → `"funnel-post"`
138
+ 4. `"event"` — for non-funnel standalone events (return value REPLACES the event)
139
+ 5. `"everything"` — array of ALL the user's events (return array to replace)
140
+ 6. **Storage phase** — data written to disk. Hooks for `event`, `user`, `scd` do NOT re-fire (already applied above). Hooks for `mirror`, `ad-spend`, `group`, `lookup` fire only in storage.
141
+
142
+ Return-value behavior:
143
+ - `event` hook: return value IS used (replaces the event)
144
+ - `everything` hook: return value IS used if it's an array (replaces event list)
145
+ - `user`, `scd-pre`, `funnel-post`: return value IGNORED — only in-place mutations work
146
+ - `funnel-pre`: return value IGNORED — mutate the `record` object in-place (e.g., `record.conversionRate = 0.9`)
147
+
148
+ ## Final output
149
+
150
+ Tell the user:
151
+ 1. Report path: `./research/hook-results.md`
152
+ 2. Verification SQL path (for user dungeons): `dungeons/user/<name>-verifications.sql`
153
+ 3. Query log path (if written): `./research/hook-query-log.txt`
154
+ 4. Pass/weak/fail counts (per dungeon if batch mode)
155
+ 5. One-line summary of the most interesting finding
156
+
157
+ If hooks failed, note that `hook-results.md` can be used as context for fixing them: "read hook-results.md and fix the failing hooks in <dungeon-file>".
@@ -0,0 +1,161 @@
1
+ # Counting Semantics — Mixpanel-Accurate Verification
2
+
3
+ Mixpanel does NOT count the way naive SQL does. The verifier (and any DuckDB query you write) must match Mixpanel's rules.
4
+
5
+ ## Core rules
6
+
7
+ | Concept | Mixpanel rule | Wrong SQL → Right SQL |
8
+ |---------|--------------|----------------------|
9
+ | Frequency / cohort by event count | Distinct calendar days, NOT total events | `COUNT(*)` → `COUNT(DISTINCT date_trunc('day', time::TIMESTAMP))` |
10
+ | Funnels | Greedy single-pass, strict order, 2-second grace | NEVER hand-roll funnel SQL — use `emulateBreakdown` |
11
+ | AVG / SUM / MIN / MAX | Skip null and non-numeric from BOTH num and denom | Always wrap in `TRY_CAST(prop AS DOUBLE)` |
12
+ | Attribution | Cap at 10 touchpoints in lookback | Use `emulateBreakdown` with `attributedBy` |
13
+ | Conversion window | Strict `<` boundary | Read `Funnel.conversionWindowDays` and respect it |
14
+
15
+ Full rules: see [HOOKS.md Section 2](../../../../HOOKS.md#2-how-mixpanel-counts-things).
16
+
17
+ ## When to use the emulator vs DuckDB
18
+
19
+ The emulator (`emulateBreakdown` from `@ak--47/dungeon-master/verify`) implements Mixpanel's rules natively. **ALWAYS use the emulator for funnel, frequency, aggregate, TTC, and attribution patterns.** Hand-written DuckDB queries for these pattern types diverge from what Mixpanel shows in reports — even when they look correct.
20
+
21
+ Use DuckDB ONLY for:
22
+ - Schema integrity checks (column coverage, flag detection)
23
+ - Identity-model invariants (stitch counts, pre-existing user stamping)
24
+ - Experiment invariants (variant distribution, exposure timing)
25
+ - Bespoke patterns that don't fit the emulator analyses
26
+
27
+ If you find yourself writing `WITH step1 AS ..., step2 AS ...` for a funnel, STOP — use `emulateBreakdown` with `funnelFrequency` instead.
28
+
29
+ ## Emulator analysis types
30
+
31
+ | Pattern style | Emulator type | Use when |
32
+ |--------------|---------------|----------|
33
+ | count(A) by per-user count(B) | `frequencyByFrequency` | "Insights frequency distribution by per-user count of X" |
34
+ | Funnel completion by per-user count(X) | `funnelFrequency` | "Onboarding magic number" / "engaged users complete more" |
35
+ | avg(prop X) by per-user count(B) | `aggregatePerUser` | "Average order value by sessions per user" |
36
+ | Funnel TTC by user property | `timeToConvert` | "Trial users take 4× longer than enterprise" |
37
+ | First/last touch attribution | `attributedBy` | "Conversions by Source" |
38
+ | Birth retention curves | `retention` | "Sign Up → Login on day N" — requires `cohortEvent`, `returnEvent`, `dayBuckets` |
39
+ | Per-session metrics | `sessionMetrics` | Count / duration / events distributions per session |
40
+
41
+ Cross-cutting on EVERY type: `timeBucket: 'day' | 'week' | 'month'` partitions events into UTC buckets and emits one row per period.
42
+
43
+ Quick emulator script:
44
+
45
+ ```js
46
+ import generate from './index.js';
47
+ import { emulateBreakdown } from './lib/verify/index.js';
48
+
49
+ const r = await generate('./dungeons/<path>.js');
50
+ const events = Array.from(r.eventData);
51
+ console.log(emulateBreakdown(events, {
52
+ type: 'frequencyByFrequency',
53
+ metricEvent: 'Purchase',
54
+ breakdownByFrequencyOf: 'Browse',
55
+ }));
56
+ ```
57
+
58
+ For CI-style assertions, use `verifyDungeon` with a checks array; see `tests/e2e/my-buddy-stories.test.js` for a worked example.
59
+
60
+ ## What the verifier auto-applies
61
+
62
+ - **`Funnel.conversionWindowDays` auto-applied.** When a check's `breakdown` matches a funnel by sequence, `verifyDungeon` reads `conversionWindowDays` from the funnel config and passes it to the emulator. You do NOT need to thread `conversionWindowMs` by hand for funnels declared in the dungeon.
63
+ - **`Funnel.order` auto-dispatched.** For `sequential` / `interrupt` funnels, the emulator runs the greedy single-pass engine. For other order modes (`first-fixed`, `last-fixed`, `random`, etc.), it dispatches to `evaluateAnyOrderCompletion` (set-membership check). For `random` mode, results are `verificationKind: "informational"` — Mixpanel funnel shape doesn't apply; do not assert PASS/FAIL.
64
+ - **Auto-sort means custom DuckDB queries can trust event order.** Per-user events arrive sorted ascending by time (default; opt out via `autoSortAfterEverything: false`). `LAG`/`LEAD` window functions work without explicit `ORDER BY time` in the partition.
65
+ - **Auto-promote `isStrictEvent` is silent healing — not a regression.** If a stale dungeon's funnel-step events also live in `events[]`, the validator stamps `isStrictEvent: true` and warns. Verification of those dungeons may show CHANGED standalone-event counts vs older runs — that is correct behavior, not a bug to chase.
66
+ - **Touchpoint cap = 10 enforced at generation.** `hasCampaigns: true` users get up to `maxTouchpointsPerUser` (default 10) UTM-stamped events, sampled across lifetime. Attribution checks via `attributedBy` should see realistic first/last-touch shapes, not all-stamps-at-birth.
67
+
68
+ ## Hook awareness for verification
69
+
70
+ - `injectOnNewDays(events, eventName, targetDays)` — clones events onto previously empty days. Hook authors are expected to use it for COHORT-CONDITIONAL active-day patterns only. Global active-day shape lives in `Dungeon.avgActiveDaysPerUser` (config knob), so a dungeon that uses this atom EVERYWHERE (not scoped to a cohort) is suspect — flag it.
71
+ - Engine-stamped UTMs may already exist on events. Hooks that bias attribution should OVERWRITE existing UTMs, not stamp fresh.
72
+
73
+ ## Common verification gotchas
74
+
75
+ | Symptom | Root cause | Fix |
76
+ |---------|-----------|-----|
77
+ | Cohort B shows MORE absolute post-d30 events than cohort A even though hook reduces B | Cohort B has structurally higher event volume (e.g., low-balance users check balance constantly) | Compare per-user `post / pre` ratio, not raw counts |
78
+ | Time-window hook (rainy week, etc.) inverts when measured against full-dataset avg | Born-in-dataset ramp inflates late-window baseline | Compare against neighboring days only, not full-dataset average |
79
+ | Weekend-surge hook still <1.0x weekday | Default soup `dayOfWeekWeights` dampens weekends to ~0.55x weekday | Verify against soup baseline (`wknd/wkday > 0.55 × 1.2`), not >1.0 |
80
+ | Funnel-post TTC scaling doesn't move emulator's `timeToConvert` rows | `evaluateFunnel` is greedy single-pass over full event history — picks first match per step regardless of which funnel-instance the hook touched | Document as known limitation (`H9 TTC populations present (limitation)`) |
81
+ | Hook references `profile.X` that's not a defined userProp | Validator doesn't catch undeclared profile reads — `X` resolves to `undefined` | Verify by data SPREAD (max/min, cv) instead of segment correlation |
82
+ | Two `everything` hooks where one injects events the other mutates produce wrong ratios | Hook ordering matters — injection hook ran AFTER cohort-shaping hook | Run cohort-degrading hooks AFTER all injection hooks in same `everything` block |
83
+ | `readFileSync` ENOMEM on shards >500MB | Node string cap at ~512MB | Stream-load with `readline.createInterface` over `data/PREFIX-EVENTS*.json` glob |
84
+ | Hook reads `e.event === 'login'` but cohort empty | `login` is a funnel-step event auto-promoted to `isStrictEvent: true` | Add `isStrictEvent: false` to the event config to keep standalone occurrences |
85
+
86
+ When writing per-dungeon verify scripts, follow the template in HOOKS.md §9.9. Reference proofs for all 20 vertical dungeons live at `verification/verticals/` — consult them as exemplars before authoring a new one.
87
+
88
+ **Coverage discipline:** count documented hooks in the dungeon's top-level comment block; count `check()` calls in your verify script; the two MUST match. A "NAILED 7/7" claim against an 11-hook dungeon is misleading — either add the missing checks or document the limitation explicitly in the verify script and status file.
89
+
90
+ ## Common verification mistakes
91
+
92
+ If a dungeon's frequency / funnel / TTC pattern shows WEAK or NONE in verification, check these BEFORE concluding the hook is broken:
93
+
94
+ 1. **Did you use `COUNT(*)` instead of distinct days?** Frequency-based patterns require `COUNT(DISTINCT date_trunc('day', time::TIMESTAMP))`.
95
+ 2. **Did you hand-roll funnel SQL?** Self-joins find the optimal match; Mixpanel uses greedy. Always use `emulateBreakdown` for funnels.
96
+ 3. **Are you including step events at the conversion-window boundary?** Mixpanel uses strict `<`. An event exactly at the boundary is excluded.
97
+ 4. **Did the hook scale event count without spreading across days?** `scaleEventCount(events, 'X', 3)` clones at sub-second offsets — same day. Frequency reports show ZERO movement. Use `injectOnNewDays`.
98
+ 5. **Are you averaging a sometimes-missing property with `AVG()`?** Always wrap in `TRY_CAST(prop AS DOUBLE)`.
99
+ 6. **Did you write a funnel-step event as a standalone in `events[]`?** The validator auto-promotes it to `isStrictEvent: true`. Verify the resulting standalone count matches expectations.
100
+
101
+ ## Verifier exports
102
+
103
+ Direct access to the engine's counting + funnel primitives:
104
+
105
+ ```js
106
+ import {
107
+ evaluateFunnel,
108
+ evaluateFunnelHPC, // Hold Property Constant
109
+ resolveFunnelSegment, // first / last / step segment modes
110
+ evaluateAnyOrderCompletion,
111
+ buildIdentityMap, resolveUserId, // identity resolution
112
+ countDistinctPeriods,
113
+ nullAwareAvg,
114
+ binByDistinctPeriods,
115
+ partitionByTimeBucket, // day / week / month
116
+ } from '@ak--47/dungeon-master/verify';
117
+ ```
118
+
119
+ ## Funnel option threading
120
+
121
+ When the dungeon's `Funnel` config sets these fields, `verifyDungeon` auto-applies them to matching `funnelFrequency` / `timeToConvert` checks:
122
+
123
+ | Funnel field | Verifier behavior |
124
+ |--------------|-------------------|
125
+ | `reentry: true` | Counts every completion via `result.completions` |
126
+ | `exclusionEvents: string[]` | Wraps as `exclusionSteps: [{ event }]` and terminates the funnel attempt |
127
+ | `stepFilters: { N: { prop, op, value } }` | Mutates `breakdownArgs.steps[N]` to attach the `where` clause |
128
+
129
+ ## Identity-model dungeons — pass profiles
130
+
131
+ When `avgDevicePerUser > 0` or `hasAnonIds: true`, ALWAYS pass `profiles` to `emulateBreakdown`. Without it, pre-auth `device_id` events bucket as separate "users" and your funnel/retention/attribution numbers all deflate.
132
+
133
+ ```js
134
+ const events = Array.from(result.eventData);
135
+ const profiles = Array.from(result.userProfilesData);
136
+
137
+ emulateBreakdown(events, {
138
+ type: 'funnelFrequency',
139
+ steps: ['visit_landing', 'sign_up', 'first_action'],
140
+ breakdownByFrequencyOf: 'visit_landing',
141
+ profiles, // ← REQUIRED for identity-model dungeons
142
+ });
143
+ ```
144
+
145
+ Auto-builds the device→user map via `buildIdentityMap(profiles)` (reads `device_ids` first, falls back to `anonymousIds`). For repeated calls, build once and pass `identityMap`.
146
+
147
+ ## Time-series verification (timeBucket)
148
+
149
+ For temporal trends (engineered campaigns, weekly cycles, growth shapes), add `timeBucket` to any breakdown:
150
+
151
+ ```js
152
+ emulateBreakdown(events, {
153
+ type: 'frequencyByFrequency',
154
+ metricEvent: 'Purchase',
155
+ breakdownByFrequencyOf: 'Browse',
156
+ timeBucket: 'week',
157
+ });
158
+ // → [{ period: '2024-W01', metric_freq, breakdown_freq, user_count }, ...]
159
+ ```
160
+
161
+ Use `period` to assert weekly / monthly trend shapes (e.g., "engagement rises month over month").
@@ -0,0 +1,216 @@
1
+ # Report Format
2
+
3
+ Templates and conventions for writing `./research/hook-results.md` and per-dungeon verification SQL.
4
+
5
+ ## Verdict criteria (5-tier)
6
+
7
+ - **NAILED** — Within 10% of expected value/ratio. Direction correct, magnitude precise. The story reads exactly as documented.
8
+ - **STRONG** — Within 25% of expected. Direction correct, clearly visible. An analyst would find this pattern immediately.
9
+ - **WEAK** — Within 50% of expected. Directionally correct but magnitude is off, OR sample size is too small to be conclusive.
10
+ - **NONE** — No statistically meaningful difference between cohorts. The hook has no observable effect.
11
+ - **INVERSE** — Effect goes the opposite direction from intended. The story is backwards.
12
+
13
+ NAILED and STRONG are passing verdicts. WEAK, NONE, and INVERSE are failing verdicts that require investigation.
14
+
15
+ ## Ordering: failures first
16
+
17
+ Within each dungeon section, order detailed results by verdict severity:
18
+ 1. **INVERSE** hooks first
19
+ 2. **NONE** hooks second
20
+ 3. **WEAK** hooks third
21
+ 4. **STRONG** hooks fourth
22
+ 5. **NAILED** hooks last
23
+
24
+ The summary table should also be sorted this way (INVERSE → NONE → WEAK → STRONG → NAILED). This ensures actionable issues are immediately visible at the top.
25
+
26
+ ## Single-dungeon report structure
27
+
28
+ ```markdown
29
+ # Dungeon Verification Report
30
+
31
+ **Dungeon:** `<filename>`
32
+ **Run Date:** <date>
33
+ **Users:** <count> | **Events:** <count> | **Duration:** <time>
34
+
35
+ ## Schema Validation
36
+
37
+ | Event Type | Added Columns | Coverage | Verdict |
38
+ |-----------|---------------|----------|---------|
39
+ | purchase | (none) | — | SCHEMA-PASS |
40
+ | page view | (none) | — | SCHEMA-PASS |
41
+
42
+ <if any SCHEMA-FAIL, list remediation details here>
43
+
44
+ ## Hook Summary
45
+
46
+ | # | Hook Name | Type | Expected Effect | Observed | Verdict |
47
+ |---|-----------|------|-----------------|----------|---------|
48
+ | 3 | ... | funnel-pre | ... | ... | INVERSE |
49
+ | 2 | ... | everything | ... | ... | WEAK |
50
+ | 1 | ... | event | ... | ... | NAILED |
51
+
52
+ ## Detailed Results
53
+
54
+ <hooks ordered INVERSE → NONE → WEAK → STRONG → NAILED>
55
+
56
+ ### Hook #3: <Name> (INVERSE)
57
+ ...
58
+
59
+ ### Hook #2: <Name> (WEAK)
60
+ ...
61
+
62
+ ### Hook #1: <Name> (NAILED)
63
+ ...
64
+
65
+ ## Recommendations
66
+ <For any WEAK or FAIL hooks>
67
+ ```
68
+
69
+ ## Multi-dungeon report structure
70
+
71
+ When verifying multiple dungeons, use this consolidated structure. Each dungeon gets its own section with its own summary table and detailed results, all in one file:
72
+
73
+ ```markdown
74
+ # Hook Verification Report
75
+
76
+ **Run Date:** <date>
77
+ **Dungeons verified:** <count>
78
+
79
+ ## Overall Summary
80
+
81
+ | Dungeon | Hooks | NAILED | STRONG | WEAK | NONE | INVERSE |
82
+ |---------|-------|--------|--------|------|------|---------|
83
+ | `harness-fintech.js` | 8 | 4 | 2 | 1 | 1 | 0 |
84
+ | `harness-gaming.js` | 10 | 7 | 2 | 1 | 0 | 0 |
85
+
86
+ ---
87
+
88
+ ## harness-fintech.js
89
+
90
+ **Users:** <count> | **Events:** <count> | **Duration:** <time>
91
+
92
+ ### Summary
93
+
94
+ | # | Hook Name | Type | Expected Effect | Observed | Verdict |
95
+ |---|-----------|------|-----------------|----------|---------|
96
+ | 4 | Low Balance Churn | everything | ... | ... | NONE |
97
+ | 2 | Payday Patterns | event | ... | ... | WEAK |
98
+ | 1 | Personal vs Business | user | ... | ... | NAILED |
99
+ | ... | ... | ... | ... | ... | ... |
100
+
101
+ ### Detailed Results
102
+
103
+ <hooks ordered INVERSE → NONE → WEAK → STRONG → NAILED>
104
+
105
+ ### Recommendations
106
+
107
+ <for this dungeon's WEAK/FAIL hooks>
108
+
109
+ ---
110
+
111
+ ## harness-gaming.js
112
+
113
+ <same structure, repeated per dungeon>
114
+
115
+ ---
116
+ ```
117
+
118
+ **Key rules for multi-dungeon reports:**
119
+ - The overall summary table at the top shows pass/weak/fail counts per dungeon, sorted with most failures first
120
+ - Each dungeon section is self-contained with its own summary, details, and recommendations
121
+ - Dungeon sections are ordered by failure count descending (most problems first)
122
+ - Use the dungeon filename (without path) as the section header for clarity
123
+
124
+ ## Per-hook detail block
125
+
126
+ Each hook's detailed section follows this template (same for single and multi-dungeon):
127
+
128
+ ```markdown
129
+ ### Hook #N: <Name> (<VERDICT>)
130
+
131
+ **Intent:** <what the hook is supposed to do>
132
+ **Type:** `<hook type>`
133
+ **Mechanism:** <brief description of how the code works>
134
+
135
+ **Query:**
136
+ ```sql
137
+ <the actual SQL executed>
138
+ ```
139
+
140
+ **Results:**
141
+ <paste the DuckDB output table>
142
+
143
+ **Analysis:** <interpret the numbers — does the ratio/difference match expectations?>
144
+
145
+ **Verdict:** NAILED / STRONG / WEAK / NONE / INVERSE
146
+ ```
147
+
148
+ ## Query log format
149
+
150
+ If `./research/` exists locally, write a plain-text log of every DuckDB query execution to `./research/hook-query-log.txt`. If `./research/` does not exist, skip this step entirely — do not create the directory.
151
+
152
+ Check with: `ls -d ./research/ 2>/dev/null`
153
+
154
+ Use a consistent delimited format — one block per query, separated by a ruler line. DuckDB table output is preserved verbatim:
155
+
156
+ ```
157
+ ================================================================================
158
+ DUNGEON: gaming.js
159
+ HOOK: #1 — Power users have 3x purchase amount
160
+ TYPE: everything
161
+ VERDICT: STRONG
162
+ EXPECTED: ~3x ratio between power and regular users
163
+ OBSERVED: 3.05x ratio
164
+
165
+ SQL:
166
+ SELECT segment, AVG(amount) as avg_amt, COUNT(*) as n
167
+ FROM read_json_auto('./data/verify-dungeon-EVENTS.json')
168
+ WHERE event = 'purchase'
169
+ GROUP BY segment;
170
+
171
+ OUTPUT:
172
+ ┌────────────┬─────────┬───────┐
173
+ │ segment │ avg_amt │ n │
174
+ │ varchar │ double │ int64 │
175
+ ├────────────┼─────────┼───────┤
176
+ │ power_user │ 45.20 │ 3841 │
177
+ │ regular │ 14.80 │ 12037 │
178
+ └────────────┴─────────┴───────┘
179
+
180
+ ANALYSIS: Power users avg $45.20 vs regular $14.80 = 3.05x ratio
181
+ ================================================================================
182
+ ```
183
+
184
+ In batch mode (multiple dungeons), all queries across all dungeons go into the same file sequentially. The format is grep-friendly:
185
+
186
+ ```bash
187
+ grep "^VERDICT:" research/hook-query-log.txt # all verdicts
188
+ grep -B4 "^VERDICT: FAIL" research/hook-query-log.txt # failing hooks with context
189
+ grep "^DUNGEON:" research/hook-query-log.txt # list of dungeons queried
190
+ ```
191
+
192
+ ## Verification SQL file (mandatory for user dungeons)
193
+
194
+ When verifying a dungeon in `dungeons/user/`, write a standalone DuckDB SQL file alongside the dungeon at `dungeons/user/<name>-verifications.sql`. This file is the reproducible verification artifact — anyone can re-run it against fresh data.
195
+
196
+ Follow the format in `verification/verticals/`:
197
+
198
+ ```sql
199
+ -- ============================================================================
200
+ -- <name>.js — Hook Verification SQL (N hooks)
201
+ -- ============================================================================
202
+ -- USAGE:
203
+ -- 1. node scripts/verify-runner.mjs dungeons/user/<name>.js verify-<name>
204
+ -- 2. duckdb < dungeons/user/<name>-verifications.sql
205
+ -- 3. rm -f verify-<name>-*
206
+ -- ============================================================================
207
+
208
+ -- HOOK N: NAME (TYPE)
209
+ -- PATTERN: <from dungeon JSDoc>
210
+ -- R1 RESULT: <observed> => <verdict>
211
+ <SQL>;
212
+ ```
213
+
214
+ Each query block includes the pattern description, observed result, and verdict as SQL comments. This makes the file self-documenting and grep-friendly.
215
+
216
+ **This step is mandatory for user dungeons.** Vertical dungeons already have their SQL in `verification/verticals/`. User dungeons keep theirs co-located with the dungeon file.