@ak--47/dungeon-master 1.8.0 → 1.8.2

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 (33) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +9 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +35 -34
  3. package/.claude/skills/create-project/SKILL.md +6 -0
  4. package/.claude/skills/headless-build/SKILL.md +21 -11
  5. package/.claude/skills/powertools/SKILL.md +6 -2
  6. package/.claude/skills/release-check/SKILL.md +27 -2
  7. package/.claude/skills/verify-dungeon/SKILL.md +32 -13
  8. package/.claude/skills/verify-dungeon/references/alignment-contract.md +110 -0
  9. package/.claude/skills/verify-dungeon/references/counting-semantics.md +29 -16
  10. package/.claude/skills/verify-dungeon/references/report-format.md +23 -9
  11. package/.claude/skills/verify-dungeon/references/sql-recipes.md +135 -225
  12. package/.claude/skills/warehouse-metrics/SKILL.md +6 -0
  13. package/.claude/skills/write-hooks/SKILL.md +61 -48
  14. package/CHANGELOG.md +82 -0
  15. package/HOOKS.md +105 -47
  16. package/README.md +41 -1
  17. package/docs/guides/1.8.1-upgrade-guide.md +153 -0
  18. package/docs/guides/1.8.2-upgrade-guide.md +110 -0
  19. package/lib/generators/events.js +6 -0
  20. package/lib/generators/funnels.js +16 -0
  21. package/lib/hook-helpers/shape.js +73 -17
  22. package/lib/hook-patterns/attributed-by-source.js +4 -3
  23. package/lib/hook-patterns/funnel-frequency-breakdown.js +4 -7
  24. package/lib/orchestrators/user-loop.js +82 -15
  25. package/lib/verify/counting.js +7 -10
  26. package/lib/verify/emulate-breakdown.js +48 -29
  27. package/lib/verify/funnel-engine.js +93 -40
  28. package/lib/verify/identity.js +32 -9
  29. package/lib/verify/story-runner.js +93 -30
  30. package/lib/verify/verify-dungeon.js +4 -1
  31. package/package.json +1 -1
  32. package/scripts/verify-stories.mjs +3 -3
  33. package/types.d.ts +9 -5
@@ -12,26 +12,28 @@ Automatic warehouse audits run even without stories. Its history buckets can
12
12
  precede the user-event window; sparse point-in-time tables intentionally omit
13
13
  unchanged rows. Do not apply cadence or user-population counting rules to them.
14
14
 
15
- # Counting Semantics Mixpanel-Accurate Verification
15
+ # Counting semantics and local proof limits
16
16
 
17
- Mixpanel does NOT count the way naive SQL does. The verifier (and any DuckDB query you write) must match Mixpanel's rules.
17
+ Read the [1.8.1 verification contract](alignment-contract.md) first. The verifier
18
+ implements selected source-derived counting rules. Record explicit report options
19
+ and remaining gaps; a passing local assertion does not establish universal parity.
18
20
 
19
21
  ## Core rules
20
22
 
21
23
  | Concept | Mixpanel rule | Wrong SQL → Right SQL |
22
24
  |---------|--------------|----------------------|
23
- | Frequency / cohort by event count | Distinct calendar days, NOT total events | `COUNT(*)` `COUNT(DISTINCT date_trunc('day', time::TIMESTAMP))` |
24
- | Funnels | Greedy single-pass, strict order, 2-second grace (`history.cpp` `OUT_OF_ORDER_MILLISECONDS = 2000`) | NEVER hand-roll funnel SQL use `emulateBreakdown` |
25
+ | Frequency / cohort by event count | Report-dependent: raw totals, calendar days, or rolling periods | Set the counting algorithm explicitly; distinct UTC days do not prove rolling-frequency parity |
26
+ | Funnels | Ordered histories, restart/shared-edge behavior, inclusive 2000ms completion grace | Use `emulateBreakdown` with explicit window, order, and reentry; local totals default to `reentry: false` |
25
27
  | AVG / SUM / MIN / MAX | Skip null and non-numeric from BOTH num and denom (`normal_query.cpp:1718-1733`) | Always wrap in `TRY_CAST(prop AS DOUBLE)` |
26
28
  | Attribution | Cap at 10 touchpoints in lookback (`attributed_value_reader.cpp:16` `TOUCHPOINTS_LIMIT`) | Use `emulateBreakdown` with `attributedBy` |
27
29
  | Conversion window | Strict `<` boundary (`conversion_window.cpp:48` `t1 < t2 + 1000*len`) | Read `Funnel.conversionWindowDays` and respect it |
28
- | Sessions | 3-trigger split: timeout `>`, max duration `>`, day-idx change (`session_query.cpp:906-911`) | Trust pre-stamped `session_id`; group by `(user, session_id)` |
30
+ | Sessions | 3-trigger split: timeout `>`, max duration `>`, day-idx change (`session_query.cpp:906-911`) | Derive from the full resolved user stream before HPC partitioning; local UTC / 30-minute / 24-hour defaults |
29
31
  | Retention | Birth-anchored, ms-strict gate (default `birth_can_retain=false` → `<`), bucketed by `floor((ret−birth)/unit)` (`retention_query.cpp:1097-1109,1228-1231`) | Use `emulateBreakdown` with `retention` |
30
32
 
31
33
  **Known divergences from Mixpanel C++** (1.6.0):
32
34
  - `countDistinctPeriods` default = `algorithm: 'calendar'` (UTC bucket).
33
35
  Mixpanel C++ (`addiction_query.cpp:359`) uses ROLLING window. Pass
34
- `algorithm: 'rolling'` for exact Frequency-Distribution parity.
36
+ `algorithm: 'rolling'` when that is the intended report algorithm; validate the remaining report options separately.
35
37
  - Touchpoint sampling: generator stamps uniform-random across user
36
38
  lifetime; verifier reads last-N before conversion (matches C++).
37
39
  For users with ≤10 attribution events lifetime, no divergence.
@@ -42,7 +44,10 @@ Full rules: see [HOOKS.md Section 2](../../../../HOOKS.md#2-how-mixpanel-counts-
42
44
 
43
45
  ## When to use the emulator vs DuckDB
44
46
 
45
- The emulator (`emulateBreakdown` from `@ak--47/dungeon-master/verify`) implements Mixpanel's rules natively. **ALWAYS use the emulator for funnel, frequency, aggregate, TTC, attribution, retention, lifecycle, flows, sessions, and event-breakdown patterns.** Hand-written DuckDB queries for these pattern types diverge from what Mixpanel shows in reports — even when they look correct.
47
+ Prefer `emulateBreakdown` from `@ak--47/dungeon-master/verify` for supported report
48
+ types. Inspect its options against the independent report specification. If a
49
+ required semantic is unsupported, report the gap; do not silently substitute SQL
50
+ or a different metric and call the original report verified.
46
51
 
47
52
  Use DuckDB ONLY for:
48
53
  - Schema integrity checks (column coverage, flag detection)
@@ -110,7 +115,9 @@ For CI-style assertions, use `verifyDungeon` with a checks array; see `tests/e2e
110
115
 
111
116
  - **`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.
112
117
  - **`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.
113
- - **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.
118
+ - **SQL still needs explicit ordering.** The engine auto-sorts output by default,
119
+ but SQL row order is unspecified. Use `ORDER BY time` in `LAG`/`LEAD` windows
120
+ and define tie handling when the report depends on it.
114
121
  - **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.
115
122
  - **Touchpoint cap = 10 enforced at generation.** `switches.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. The verifier's `attributedBy` reads last-N before conversion (matches Mixpanel); the generator samples uniform across lifetime. For users with >10 attribution events lifetime, expect minor divergence on multi-conversion users.
116
123
 
@@ -126,7 +133,7 @@ For CI-style assertions, use `verifyDungeon` with a checks array; see `tests/e2e
126
133
  | 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 |
127
134
  | 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 |
128
135
  | 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 |
129
- | 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)`) |
136
+ | Funnel-post TTC scaling does not move the report | Completed histories can combine instances or restart; the scaled run may not be the reported history | Inspect completed histories under explicit options and compare paired baseline/treatment; do not accept by code inspection |
130
137
  | 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 |
131
138
  | 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 |
132
139
  | `readFileSync` ENOMEM on shards >500MB | Node string cap at ~512MB | Stream-load with `readline.createInterface` over `data/PREFIX-EVENTS*.json` glob |
@@ -140,7 +147,7 @@ When writing per-dungeon verify scripts, follow the template in HOOKS.md §9.9.
140
147
 
141
148
  If a dungeon's frequency / funnel / TTC pattern shows WEAK or NONE in verification, check these BEFORE concluding the hook is broken:
142
149
 
143
- 1. **Did you use `COUNT(*)` instead of distinct days?** Frequency-based patterns require `COUNT(DISTINCT date_trunc('day', time::TIMESTAMP))`.
150
+ 1. **Did you choose the report's counting unit?** Raw totals, distinct UTC days, and rolling periods answer different questions; set the algorithm explicitly.
144
151
  2. **Did you hand-roll funnel SQL?** Self-joins find the optimal match; Mixpanel uses greedy. Always use `emulateBreakdown` for funnels.
145
152
  3. **Are you including step events at the conversion-window boundary?** Mixpanel uses strict `<`. An event exactly at the boundary is excluded.
146
153
  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`.
@@ -175,12 +182,13 @@ When the dungeon's `Funnel` config sets these fields, `verifyDungeon` auto-appli
175
182
  | `exclusionEvents: string[]` | Wraps as `exclusionSteps: [{ event }]` and terminates the funnel attempt |
176
183
  | `stepFilters: { N: { prop, op, value } }` | Mutates `breakdownArgs.steps[N]` to attach the `where` clause |
177
184
 
178
- ## Identity-model dungeons pass profiles
185
+ ## Identity-model dungeons: profiles and emitted links
179
186
 
180
187
  When `identity.avgDevicePerUser > 0` (or the deprecated `hasAnonIds: true`),
181
- ALWAYS pass `profiles` to `emulateBreakdown`. Without it, pre-auth
182
- `device_id` events bucket as separate "users" and your funnel/retention/
183
- attribution numbers all deflate.
188
+ pass `profiles` for profile-property segmentation. For identity proof, build an
189
+ explicit `identityMap` from valid ordinary both-ID events in the emitted stream.
190
+ Profile device pools alone do not establish a link. Later ordinary Login events
191
+ can link earlier device-only rows retrospectively; report conflicts and ID limits.
184
192
 
185
193
  **v1.5.1 anonymous non-converter semantic:** born-in-dataset users who
186
194
  never reach an `isAuthEvent` get `_drop: true` on their profile.
@@ -197,11 +205,16 @@ emulateBreakdown(events, {
197
205
  type: 'funnelFrequency',
198
206
  steps: ['visit_landing', 'sign_up', 'first_action'],
199
207
  breakdownByFrequencyOf: 'visit_landing',
200
- profiles, // ← REQUIRED for identity-model dungeons
208
+ profiles,
209
+ identityMap, // prepared from emitted both-ID evidence for this run
201
210
  });
202
211
  ```
203
212
 
204
- 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`.
213
+ In 1.8.2, automatic mapping uses emitted both-ID records before report filtering.
214
+ The public `buildIdentityMap(profiles)` helper still reads profile pools when a
215
+ caller explicitly requests that override. It cannot prove that a link survived
216
+ generation or ingest. Imported historical identity can become query-visible later;
217
+ verify readiness before accepting a local/live comparison.
205
218
 
206
219
  ## Time-series verification (timeBucket)
207
220
 
@@ -2,6 +2,14 @@
2
2
 
3
3
  Templates and conventions for writing `hook-results.md` and per-dungeon verification SQL. For user dungeons these live in the dungeon's folder (`dungeons/user/<name>/`); otherwise in `./research/`. See [SKILL.md "Artifact location"](../SKILL.md).
4
4
 
5
+ Apply the [1.8.1 verification contract](alignment-contract.md). Keep the runner's
6
+ computed verdict, semantic correctness, and evidence sufficiency in separate
7
+ columns. Record the independent report definition, explicit options, baseline and
8
+ neutral-control measurements, eligible users/converters, and source-derived scope.
9
+ `INSUFFICIENT_EVIDENCE` is an unresolved acceptance status, not a new runner tier
10
+ and not a pass or measured effect failure. Preserve actual deployment reports;
11
+ offline and dry-run results must not replace live outcomes.
12
+
5
13
  ## Verdict criteria (5-tier)
6
14
 
7
15
  Every report records the exact data prefix and retained artifact paths. Separate
@@ -25,11 +33,13 @@ Verdicts are **mechanical** — computed by `scripts/verify-stories.mjs` from ea
25
33
 
26
34
  - **NAILED** — observed within ±10% of `target`.
27
35
  - **STRONG** — passes `floor` (or `target` when no floor is declared).
28
- - **WEAK** fails `floor` but effect direction is correct, **or** the selected cohort is smaller than `minCohort`. The population floor is a hard cap: a 12-user cohort can never score NAILED, no matter how clean its ratio.
36
+ - **WEAK** - fails `floor` but effect direction is correct, or a supported selected-row population field is below `minCohort`. Inspect actual independent users and converters separately; bespoke denominators may not be guarded.
29
37
  - **NONE** — no measurable effect, or the selection matched no rows.
30
38
  - **INVERSE** — effect direction is opposite the assertion.
31
39
 
32
- Story verdict = worst assertion verdict. NAILED and STRONG are passing; WEAK, NONE, and INVERSE fail and require investigation.
40
+ Story verdict = worst assertion verdict. NAILED and STRONG pass the mechanical
41
+ gate; WEAK, NONE, and INVERSE fail it. Acceptance also requires correct report
42
+ semantics, neutral controls, and sufficient evidence, even for a passing target.
33
43
 
34
44
  Hand-assigned verdicts appear only in the legacy no-stories fallback and MUST follow the same definitions: derive a target from the hook's knob constants, compute the band the observed value lands in, and state the derivation in the detail block — never assign a tier by feel.
35
45
 
@@ -46,7 +56,10 @@ The summary table should also be sorted this way (INVERSE → NONE → WEAK →
46
56
 
47
57
  ## Single-dungeon report structure
48
58
 
49
- For story-backed dungeons, `hook-results.md` **renders the runner's JSON**: run `verify-stories.mjs --json` and build the Hook Summary table directly from its per-story records (story id, hook number, archetype, observed vs target per assertion, computed verdict). Do not recompute verdicts the runner already settled. Detailed Results blocks exist only for stories below STRONG, `duckdb`-type assertions, and legacy no-stories hooks.
59
+ For story-backed dungeons, render the runner's JSON without changing computed
60
+ verdicts. Add semantic and evidence status for every story. Include detailed
61
+ blocks for misses, bespoke SQL, legacy hooks, and semantic or evidence gaps,
62
+ including those found in mechanically passing stories.
50
63
 
51
64
  ```markdown
52
65
  # Dungeon Verification Report
@@ -62,15 +75,15 @@ For story-backed dungeons, `hook-results.md` **renders the runner's JSON**: run
62
75
  | purchase | (none) | — | SCHEMA-PASS |
63
76
  | page view | (none) | — | SCHEMA-PASS |
64
77
 
65
- <if any SCHEMA-FAIL, list remediation details here>
78
+ <fail every undeclared column, even at 100% coverage; list remediation here>
66
79
 
67
80
  ## Hook Summary
68
81
 
69
- | # | Hook Name | Type | Expected Effect | Observed | Verdict |
70
- |---|-----------|------|-----------------|----------|---------|
71
- | 3 | ... | funnel-pre | ... | ... | INVERSE |
72
- | 2 | ... | everything | ... | ... | WEAK |
73
- | 1 | ... | event | ... | ... | NAILED |
82
+ | # | Hook Name | Expected | Observed | Runner Verdict | Semantics | Evidence |
83
+ |---|-----------|----------|----------|----------------|-----------|----------|
84
+ | 3 | ... | ... | ... | INVERSE | MATCH | SUFFICIENT |
85
+ | 2 | ... | ... | ... | WEAK | MATCH | INSUFFICIENT_EVIDENCE |
86
+ | 1 | ... | ... | ... | NAILED | MATCH | SUFFICIENT |
74
87
 
75
88
  ## Detailed Results
76
89
 
@@ -141,6 +154,7 @@ When verifying multiple dungeons, use this consolidated structure. Each dungeon
141
154
  **Key rules for multi-dungeon reports:**
142
155
  - The overall summary table at the top shows pass/weak/fail counts per dungeon, sorted with most failures first
143
156
  - Each dungeon section is self-contained with its own summary, details, and recommendations
157
+ - Include the same semantic and evidence columns used in the single-dungeon template; report insufficient-evidence counts separately from runner tiers.
144
158
  - Dungeon sections are ordered by failure count descending (most problems first)
145
159
  - Use the dungeon filename (without path) as the section header for clarity
146
160