@ak--47/dungeon-master 1.7.0 → 1.8.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 (43) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +30 -11
  2. package/.claude/skills/create-dungeon/SKILL.md +84 -44
  3. package/.claude/skills/create-project/SKILL.md +28 -3
  4. package/.claude/skills/create-project/context.mjs +89 -0
  5. package/.claude/skills/create-project/provision.mjs +1 -60
  6. package/.claude/skills/headless-build/SKILL.md +39 -12
  7. package/.claude/skills/powertools/SKILL.md +26 -3
  8. package/.claude/skills/release-check/SKILL.md +124 -0
  9. package/.claude/skills/verify-dungeon/SKILL.md +103 -29
  10. package/.claude/skills/verify-dungeon/references/alignment-contract.md +84 -0
  11. package/.claude/skills/verify-dungeon/references/counting-semantics.md +41 -16
  12. package/.claude/skills/verify-dungeon/references/report-format.md +41 -10
  13. package/.claude/skills/verify-dungeon/references/sql-recipes.md +171 -226
  14. package/.claude/skills/warehouse-metrics/GAPS-template.md +34 -0
  15. package/.claude/skills/warehouse-metrics/SKILL.md +111 -0
  16. package/.claude/skills/warehouse-metrics/deploy.mjs +651 -0
  17. package/.claude/skills/write-hooks/SKILL.md +94 -51
  18. package/CHANGELOG.md +183 -0
  19. package/HOOKS.md +165 -18
  20. package/README.md +265 -1
  21. package/docs/guides/1.8.0-upgrade-guide.md +151 -0
  22. package/docs/guides/1.8.1-upgrade-guide.md +153 -0
  23. package/dungeons/technical/warehouse.js +187 -0
  24. package/index.js +116 -2
  25. package/lib/core/config-validator.js +21 -0
  26. package/lib/core/dungeon-loader.js +1 -1
  27. package/lib/core/storage.js +51 -3
  28. package/lib/generators/events.js +6 -0
  29. package/lib/generators/funnels.js +15 -0
  30. package/lib/generators/standalone.js +248 -0
  31. package/lib/generators/warehouse.js +828 -0
  32. package/lib/hook-helpers/shape.js +73 -17
  33. package/lib/orchestrators/mixpanel-sender.js +27 -2
  34. package/lib/orchestrators/user-loop.js +83 -15
  35. package/lib/templates/story-spec.schema.json +41 -16
  36. package/lib/utils/utils.js +37 -12
  37. package/lib/verify/funnel-engine.js +66 -26
  38. package/lib/verify/index.js +1 -0
  39. package/lib/verify/story-runner.js +71 -8
  40. package/lib/verify/warehouse.js +683 -0
  41. package/package.json +4 -2
  42. package/scripts/verify-stories.mjs +150 -44
  43. package/types.d.ts +312 -9
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: write-hooks
3
3
  description: Use when an existing dungeon needs engineered story trends or "magic number" patterns — writes the `hook` function using atom helpers and high-level patterns. Adds no new event flags; never mutates the schema.
4
- argument-hint: [path/to/dungeon.js] [free-text story / trend description]
4
+ argument-hint: '[path/to/dungeon.js] [free-text story / trend description]'
5
5
  model: claude-opus-4-6
6
6
  effort: max
7
7
  ---
@@ -28,8 +28,8 @@ In scope:
28
28
 
29
29
  Out of scope:
30
30
  - Schema changes (events, properties, funnels, superProps, userProps).
31
- Only modify schema if the hook can't possibly work without a new field —
32
- and even then, prefer changing the value enumeration over adding a new field.
31
+ Send missing fields or enumeration changes back to `/create-dungeon` for an
32
+ explicitly authorized schema edit before continuing. This skill never changes schema.
33
33
  - New top-level config knobs.
34
34
  - Removing the `hook: function...` body to start over with a new schema.
35
35
 
@@ -39,14 +39,15 @@ off, mix shift drags the blended rate) are better architected as initial
39
39
  conditions — duplicate funnels with swapped steps/props/`conversionRate`/
40
40
  `timeToConvert`/`weight` (see the "Structural trend engineering" section in
41
41
  `create-dungeon`). If a story reduces to structure, recommend the funnel
42
- change back to the schema instead of writing a hook to fight the engine
43
- the knob IS the expected value, which makes the story band knob-derivable
44
- (NAILED-capable) instead of confounded (STRONG-capped). Hooks are for
42
+ change back to the schema instead of writing a hook to fight the engine.
43
+ Derive the expected effect from the knob, then verify the report against a neutral
44
+ control; finite budgets and competing histories can change the measured effect. Hooks are for
45
45
  within-cohort behavior: segments doing more/less of something over time,
46
46
  property values that differ by cohort, injected bursts, lifecycle waves.
47
47
 
48
48
  ## Reference reading
49
49
 
50
+ - [1.8.1 verification contract](../verify-dungeon/references/alignment-contract.md) - independent reports, neutral controls, counting boundaries, and evidence sufficiency.
50
51
  - `lib/hook-helpers/index.js` — atoms (cohort, mutate, timing, inject,
51
52
  identity). One file per group; full JSDoc on each atom.
52
53
  - `lib/hook-patterns/index.js` — high-level recipes (one per Mixpanel
@@ -68,15 +69,15 @@ property values that differ by cohort, injected bursts, lifecycle waves.
68
69
  Hooks fire in this order per user (see `CLAUDE.md` for the canonical reference):
69
70
 
70
71
  1. `"user"` — profile created. Mutate in place; return ignored.
71
- 2. `"scd-pre"` — SCD entries created. Mutate in place OR return new array.
72
+ 2. `"scd-pre"` — SCD entries created. Mutate in place; return ignored.
72
73
  3. For each funnel: `"funnel-pre"` → `"event"` (per step) → `"funnel-post"`.
73
74
 
74
- **`funnel-pre` is now reliable for temporal patterns.** Usage funnels advance a
75
- cursor after each run, so successive `meta.firstEventTime` values spread across
76
- the user's active window. Persona and world-event modifiers apply BEFORE the
77
- hook the hook has final authority on `conversionRate`, `timeToConvert`, and
78
- `props`.
79
- 4. `"event"` — for non-funnel standalone events. Return value REPLACES the event.
75
+ **Usage anchors do not accumulate previous funnel TTC.** Each usage run samples
76
+ from its eligible window or active day after onboarding. `meta.firstEventTime`
77
+ is the supplied anchor, not necessarily the first emitted event time. Persona
78
+ and world-event modifiers apply before `funnel-pre`; the hook can mutate
79
+ `conversionRate`, `timeToConvert`, and `props` within engine constraints.
80
+ 4. `"event"` — for non-funnel user events from `events[]`. Return value REPLACES the event.
80
81
  5. `"everything"` — array of ALL the user's events. Return array to replace.
81
82
 
82
83
  **Most engineered trends belong in `everything`.** It sees the full user stream,
@@ -86,6 +87,36 @@ and you can mutate freely.
86
87
  Storage-only hooks (`ad-spend`, `group`, `mirror`, `lookup`) fire later in the
87
88
  pipeline and don't see the same `meta` shape.
88
89
 
90
+ ### Cadence streams and warehouse rows (v1.8.0)
91
+
92
+ These hooks sit outside the per-user sequence and never enter `everything`.
93
+ Neither has person metadata (`meta.profile`, auth state, sessions, or SCDs).
94
+
95
+ - `standaloneEvents`: `type === 'standalone'` fires on storage push before the
96
+ user loop. Read `meta.spec` and `meta.config` to identify the stream. Return
97
+ the record object or an array of records. Returning `undefined` drops it.
98
+ Mutating without returning is therefore insufficient. Preserve required keys
99
+ and use fresh `insert_id` values for clones. Its synthetic `distinct_id` is a
100
+ series identifier, never a person or a retention cohort.
101
+ - `warehouseMetrics`: `type === 'warehouse'` fires after user generation, once
102
+ per materialized row. Mutate the row in place; its return value is ignored.
103
+ Keep the time column and group keys stable. Modify only the value column and
104
+ declared extra columns. Meta includes `spec`, `config`, `metricName`,
105
+ `bucketIndex`, `bucketCount`, `grain`, `seriesKey`, `isBackfill`, and `raw`.
106
+ `raw` describes plus/minus source aggregates before scaling, noise, and carry.
107
+
108
+ Do not add either schema here. Send missing `standaloneEvents` properties or
109
+ warehouse `columns` back to `/create-dungeon`. Warehouse sources consume user
110
+ `events[]` only, including both plus and minus legs; they cannot consume cadence
111
+ streams. Standalone value functions use tick context, warehouse column functions
112
+ use bucket context; neither supplies a user profile.
113
+
114
+ Verify standalone stories with disk-backed `duckdb` assertions against
115
+ `{{PREFIX}}-STANDALONE*.json`. The user-event emulator and `--in-memory` CLI mode
116
+ do not evaluate this stream. Warehouse stories can use `warehouse` assertions
117
+ or `warehouse-stats` assertions; automatic warehouse audits also run without
118
+ stories. Hand off to `/verify-dungeon` with an explicit artifact prefix.
119
+
89
120
  ## Hook meta — identity context
90
121
 
91
122
  Inside `funnel-pre` and `funnel-post`:
@@ -156,7 +187,7 @@ if (type === 'funnel-post' && meta.experiment) {
156
187
  | cohort | `hashCohort(id, pct)` | Deterministic pct% cohort (0–100 scale). **Use this first for hidden cohorts** — replaces ad-hoc `charCodeAt % N`. When one dungeon needs several NON-overlapping cohorts, gate on disjoint `hashFloat(uid)` bands instead (e.g. `[0, 0.45)`, `[0.45, 0.70)`) |
157
188
  | shape | `applyLifecycleWave(events, uid, opts)` | Dormancy window + resurrection burst for Lifecycle reports. When-to-use: the story is "users go quiet, then come back". Gap discipline: ONE stray value moment inside the window destroys the Resurrected read — size `dormantDays` to cover ≥2 whole lifecycle periods, keep the window inside the user's lifespan |
158
189
  | shape | `applyPathBias(events, uid, opts)` | Inject a Flows path after the user's FIRST anchor occurrence. When-to-use: the story is "X% of users take this route". `share` is a 0–1 FRACTION (not `hashCohort`'s pct scale); needs ~≥0.20–0.25 to survive Sankey top-3-per-level pruning; per-step gaps clamped ≥1s so ordering survives |
159
- | shape | `applySessionShape(events, uid, opts)` | Retime the whole stream into `sessionsPerWeek` clusters of `sessionMinutes`. When-to-use: session-duration/cadence stories (sessionMetrics reads). Retiming ONLY no adds/drops; intra-session gaps stay <28min, inter-session >30min, no cluster crosses UTC midnight. Combine with `hashCohort` for per-role shapes; call BEFORE `applyPathBias` so injected paths keep their own tight gaps |
190
+ | shape | `applySessionShape(events, uid, opts)` | Retime the same records for session-duration/cadence stories. Pass known dataset bounds; explicit-bound mode rejects insufficient capacity before mutation. Legacy unbounded overfull layouts can merge sessions or cross dataset end. Call BEFORE `applyPathBias` so injected paths keep their own tight gaps. |
160
191
 
161
192
  ### Hook anti-patterns
162
193
 
@@ -206,8 +237,9 @@ writing a custom hook:
206
237
  No special hook needed; engineer cohort behavior via `engagementDecay`,
207
238
  `dropEventsWhere`, or per-user filtering in `everything`.
208
239
  - **Session metrics** ("avg session has 6 events, lasts 4 minutes") — verify
209
- with `emulateBreakdown({ type: 'sessionMetrics' })`. Trust pre-stamped
210
- `session_id`. Engineer via `avgEventsPerUserPerDay` + `engagementDecay`.
240
+ with `emulateBreakdown({ type: 'sessionMetrics' })`. Derive sessions from
241
+ timestamps; stamped `session_id` is diagnostic or explicit legacy mode.
242
+ Engineer with `applySessionShape`, passing known dataset bounds.
211
243
  - **Reentry funnels** ("power users complete the funnel 3+ times") — set
212
244
  `Funnel.reentry: true` (verifier hint). Engineer multiple completions via
213
245
  `funnel-post` injecting cloned funnel sequences for that cohort.
@@ -224,9 +256,10 @@ writing a custom hook:
224
256
  breakdown with `timeBucket: 'week'`. Engineer via temporal-windowed hooks
225
257
  using `DATASET_START.add(N, 'days')`.
226
258
  - **Identity-model dungeons** — when `identity.avgDevicePerUser > 0`
227
- (or the deprecated `hasAnonIds: true`), ALWAYS pass `profiles` to
228
- verification. Auto-builds identity map merging pre-auth `device_id`
229
- events with post-auth `user_id`.
259
+ (or the deprecated `hasAnonIds: true`), pass `profiles` for profile segments.
260
+ For identity proof, pass an explicit `identityMap` derived from emitted valid
261
+ both-ID events. Profile pools alone establish no link; the compatibility helper
262
+ can merge users without emitted stitch evidence. See the shared contract.
230
263
 
231
264
  **Schema-first reminder:** exclusion events must be declared in `events[]`
232
265
  before referencing them as `Funnel.exclusionEvents` — the validator throws
@@ -240,9 +273,9 @@ emulator can re-derive.
240
273
  | Pattern | Mixpanel analysis | Hook type | Caveat (HOOKS.md) |
241
274
  |---------|-------------------|-----------|-------------------|
242
275
  | `applyFrequencyByFrequency` | Insights — count(A) by per-user count(B) | everything | `binBy` defaults to `'distinctDays'` (v1.6) — bins match Mixpanel's per-user distinct-day counting, not raw event totals |
243
- | `applyFunnelFrequencyBreakdown` | Funnels completion by per-user count(X) | funnel-post | When funnels share a step prefix, restrict scaling to the target funnel scaling every instance lets first-occurrence funnel evaluation assemble chains across unscaled instances and the ratio never reaches the report |
276
+ | `applyFunnelFrequencyBreakdown` | Funnels - completion by per-user count(X) | funnel-post | Restrict scaling to the target funnel. Completed histories can combine competing instances; verify explicit report options and paired lift instead of assuming the per-run factor equals the report ratio. |
244
277
  | `applyAggregateByBin` | Insights — avg(prop X) by per-user count(B) | everything | Same `binBy: 'distinctDays'` default as above |
245
- | `applyTTCBySegmentV2` | Funnel TTC — broken down by user-property segment | everything | v1 (`applyTTCBySegment`, funnel-post) is **deprecated**: Mixpanel TTC reads each step's FIRST occurrence per user, so per-run gap scaling only reaches the report on `isFirstFunnel` runs. V2 finds the greedy first sequence (`findFirstSequence`) and scales that |
278
+ | `applyTTCBySegmentV2` | Funnel TTC by user-property segment | everything | v1 (`applyTTCBySegment`, funnel-post) is deprecated. V2 scales `findFirstSequence`; verify the report's completed histories separately, including restart, grace, windows, and reentry. Neither helper guarantees the requested report ratio. |
246
279
  | `applyAttributedBySource` | Conversions by Source (first/last touch) | everything | OVERWRITES the engine-stamped touch the chosen model reads; never stamps UTMs onto unstamped events (would blow the `maxTouchpointsPerUser` cap and land outside the last-10 lookback) |
247
280
 
248
281
  Use a pattern when the trend matches its analysis 1:1. Drop down to atoms when
@@ -268,13 +301,17 @@ non-negotiable design rule learned the hard way:
268
301
  visualization even though the data is there. Label-only path reads can
269
302
  INVERT when a busier cohort glues extra visible events between path steps —
270
303
  assert the share on the cohort you engineered, not globally.
304
+ The helper is append-only: `share` selects injection recipients, not exact
305
+ visible branch share. Keep competing events and measure paired branch lift.
271
306
  - **`session-shape`** (`applySessionShape`) — 30-MIN STRADDLING + MIDNIGHT
272
- RULE. Mixpanel derives sessions with a 30-min idle timeout and splits at
273
- UTC midnight. Engineered cadences must keep intra-session gaps clearly
307
+ RULE. The local default derives sessions with a 30-min idle timeout and splits
308
+ at UTC midnight, with a 24-hour maximum. Engineered cadences must keep intra-session gaps clearly
274
309
  UNDER 30min and inter-session gaps clearly OVER it — a gap that straddles
275
310
  the timeout makes session counts jitter across runs. Never let an
276
311
  engineered session cross UTC midnight (the day split cuts it in two). The
277
- atom guarantees all three; hand-rolled retiming must too.
312
+ bounded helper checks placement capacity before mutation. Legacy unbounded
313
+ overfull requests can merge sessions. Pass known bounds and measure derived
314
+ sessions; do not infer dataset end from the last observed event.
278
315
  - **`composition-drift`** — the breakdown's SHARE of a segment moves over
279
316
  time while totals stay flat (e.g. plan-mix shifts toward premium). Engineer
280
317
  by flipping an existing property value on a date-gated cohort, never by
@@ -309,9 +346,9 @@ events.push(clone); // ✅ inject from template
309
346
  return events.filter(e => !shouldDrop(e)); // ✅ filter inside `everything`
310
347
  ```
311
348
 
312
- If a trend genuinely needs a new property and the schema doesn't have it, add
313
- the property to the EVENT CONFIG with a default value (typically `[null]` or
314
- `[false]`), not via the hook.
349
+ If a trend needs an undeclared property, stop that pattern and hand the schema
350
+ request to `/create-dungeon`. Resume only after an authorized schema edit declares
351
+ the default. Do not add the field or change its enumeration in this skill.
315
352
 
316
353
  ## Identity-aware hook patterns
317
354
 
@@ -433,8 +470,9 @@ Rules:
433
470
  - `floor` must itself be derived (e.g. `target * 0.8`) — never hand-tuned to
434
471
  a run. A missed assertion means fixing the hook or the derivation, never
435
472
  relaxing the number to match output.
436
- - Set `minCohort` from the cohort math (share × numUsers × ~0.5 safety) so
437
- reduced-scale runs cap at WEAK instead of passing on noise.
473
+ - Set `minCohort` from the planned eligible population, then inspect the actual
474
+ selected-row population fields and independent users/converters. The guard
475
+ does not prove every denominator. Report insufficient evidence separately.
438
476
  - One story per pattern; story `hook` matches the doc-block numbering (`H3`).
439
477
  - The `assert` function escape hatch is discouraged — each use needs a
440
478
  comment saying why the declarative `expect` grammar can't express it.
@@ -444,12 +482,12 @@ Rules:
444
482
 
445
483
  The order of operations inside the everything hook matters when hooks interact:
446
484
 
447
- 1. **SuperProp stamping** stamp profile values onto events (always first)
485
+ 1. **Profile projection** - prefer schema-declared `stickyEventProps`; preserve projected values in clones
448
486
  2. **Temporal value mutations that DON'T need cloned events** — e.g., version stamping
449
487
  3. **Behavioral detection + event cloning** — agentic detection, KYC clones, pro clones, magic number clones
450
488
  4. **Event filtering** — churn, retention, rate-limit drops
451
- 5. **Temporal value mutations that NEED cloned events** e.g., spring price boost, gas spike, outage errors (always LAST before sort)
452
- 6. **Sort** `userEvents.sort((a, b) => new Date(a.time) - new Date(b.time))`
489
+ 5. **Temporal value mutations that NEED cloned events** - e.g., spring price boost, gas spike, outage errors (last mutation)
490
+ 6. **Return** - the engine auto-sorts after `everything` by default; no manual output sort
453
491
 
454
492
  **Why:** If a temporal mutation runs before cloning, cloned events that land in
455
493
  the temporal window miss the mutation. Moving temporal value mutations to the
@@ -459,7 +497,8 @@ end ensures ALL events in the window — original and cloned — receive the eff
459
497
 
460
498
  When a dungeon relied on deprecated config blocks (`subscription`, `attribution`,
461
499
  `features`, `geo`, `anomalies`) for properties that hooks depend on, those
462
- properties no longer appear in the data. Replace them:
500
+ properties no longer appear in the data. Request an authorized schema migration
501
+ through `/create-dungeon` first:
463
502
 
464
503
  1. Add the property to `superProps` and `userProps` with default values
465
504
  2. Assign meaningful values in the `user` hook (based on hash, persona, or profile)
@@ -476,8 +515,8 @@ control group, but not so broad they catch everyone:
476
515
  | Detection | Problem | Fix |
477
516
  |-----------|---------|-----|
478
517
  | `events.some(e => e.event === X)` with common X | 90%+ of users qualify | Require 3+ events: `events.filter(...).length >= 3` |
479
- | `charCodeAt(0) % 50 === 0` | Only 2% of users | Increase modulus denominator or use `% 10` for 10% |
480
- | `profile.tier === "premium"` | Fixed by config distribution | Adjust userProps distribution if cohort too small |
518
+ | `charCodeAt(0) % 50 === 0` | First-character distributions are biased; modulo does not imply 2% | Use `hashCohort(uid, 2)` and measure the realized population |
519
+ | `profile.tier === "premium"` | Fixed by config distribution | Request a schema distribution change if cohort is too small |
481
520
  | `earlyEvents.length >= 5` for a low-weight event | 0% qualify (impossible threshold) | Check actual distribution first, set at ~80th percentile |
482
521
 
483
522
  Target: 10-30% of users in the affected cohort for clean signal at 10K users.
@@ -516,11 +555,12 @@ Reserve drops for a single churn/retention effect per dungeon.
516
555
 
517
556
  Apply these BEFORE handing off to `/verify-dungeon`. See HOOKS.md §9 for full recipes.
518
557
 
519
- ### isStrictEvent: false is NOT optional for hook-read events
558
+ ### Opt out of strict events only when standalone occurrences are required
520
559
 
521
560
  If your hook reads `event === 'X'` and `X` is also a funnel-step event, the
522
561
  validator auto-promotes it to `isStrictEvent: true` and the engine
523
- won't emit standalone occurrences. Your cohort goes empty.
562
+ won't emit standalone occurrences. Funnel-generated occurrences remain readable;
563
+ only a cohort that depends on additional standalone traffic needs the opt-out.
524
564
 
525
565
  ```js
526
566
  // BAD — login is a funnel step + read by hook
@@ -529,16 +569,15 @@ events: [{ event: 'login', weight: 4, properties: {...} }]
529
569
  events: [{ event: 'login', weight: 4, isStrictEvent: false, properties: {...} }]
530
570
  ```
531
571
 
532
- Audit: any event referenced in the `everything` hook by name AND appearing
533
- as a funnel step needs `isStrictEvent: false`.
572
+ Audit whether the cohort needs standalone occurrences. Request any required
573
+ `isStrictEvent: false` schema edit through `/create-dungeon`; do not change it here.
534
574
 
535
575
  ### Reentry on per-instance loops
536
576
 
537
- Funnels named "X loop" / "X cycle" / "session" / repeated user behaviors
538
- need `Funnel.reentry: true`. Without it, the engine produces ONE funnel
539
- sequence per user no recurring loops. Examples that need it: workout
540
- loop, match flow, search-to-book, order fulfillment, engagement loop, tour
541
- funnel.
577
+ **`reentry` is verifier-only.** Usage volume, funnel selection, and the event
578
+ budget control generated repetitions. Set `reentry: true` for a report that
579
+ counts repeated histories; it does not generate loops. Local totals also
580
+ default to `reentry: false`, so preserve the report's explicit counting options.
542
581
 
543
582
  ### Hash-based cohorts produce textbook signals
544
583
 
@@ -547,12 +586,13 @@ deterministically:
547
586
 
548
587
  ```js
549
588
  // 2% whales with 50x trade amount → long-tail Insights distribution
550
- const isWhale = uid.charCodeAt(0) % 50 === 0;
589
+ const isWhale = hashCohort(uid, 2);
551
590
  if (isWhale && e.event === 'swap') e.trade_amount_usd *= 50;
552
591
  ```
553
592
 
554
593
  Use a large multiplier (≥10x, ideally 50x) so the signal beats soup noise.
555
- Use `% 50` for ~2% whales, `% 25` for ~4% bots, `% 10` for ~10% cohorts.
594
+ Use `hashCohort(uid, 2)`, `hashCohort(uid, 4)`, or `hashCohort(uid, 10)` for
595
+ those target percentages, then check realized eligible cohort sizes.
556
596
 
557
597
  ### Hook ordering inside `everything`
558
598
 
@@ -574,13 +614,13 @@ counts even after reduction. Either:
574
614
  ```js
575
615
  // BAD — profile.level isn't in userProps; resolves to undefined
576
616
  if (meta.profile.level >= 50) e.gold_earned *= 3;
577
- // GOOD verify by SPREAD instead, OR add level to userProps with weighted distribution
617
+ // Request a schema declaration before targeting this profile segment.
578
618
  ```
579
619
 
580
620
  When the hook references a missing profile field, you can still get the
581
621
  data spread you want (gold range), but the cohort can't be analytically
582
- recovered. Either add the userProp or rewrite the hook to use a hash
583
- cohort.
622
+ recovered. Request the missing userProp through `/create-dungeon`, or rewrite
623
+ the hook to use a hash cohort without changing schema.
584
624
 
585
625
  ## Workflow
586
626
 
@@ -618,11 +658,14 @@ cohort.
618
658
  If verify-dungeon returns WEAK, NONE, or INVERSE on any pattern at full
619
659
  fidelity, return to step 4 and refine — fix the hook or the derivation,
620
660
  never relax a threshold to match output. Iterate until all patterns score
621
- STRONG or NAILED.
661
+ STRONG or NAILED with report semantics, neutral controls, and sufficient
662
+ independent populations verified. Insufficient evidence requires more evidence,
663
+ not an automatic hook rewrite or a weaker target.
622
664
 
623
665
  ## Stopping condition
624
666
 
625
- Stop after `/verify-dungeon` reports all engineered patterns as STRONG or NAILED,
667
+ Stop after `/verify-dungeon` reports all engineered patterns as STRONG or NAILED
668
+ and the shared proof contract is satisfied,
626
669
  OR after three iterations without convergence — at that point, document what's
627
670
  still off in the dungeon's overview comment and report the gap to the user.
628
671
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,189 @@
2
2
 
3
3
  All notable changes to `@ak--47/dungeon-master`.
4
4
 
5
+ ## 1.8.1 — 2026-09-12
6
+
7
+ ### Fixed
8
+
9
+ - Align funnel counting with source-derived Mixpanel contracts: shared first/last
10
+ steps finalize correctly with or without reentry, completion grace consumes
11
+ events through the inclusive two-second boundary, and first/last-touch
12
+ breakdowns merge properties across reached steps.
13
+ - Derive session boundaries from the full event stream before hold-property-constant
14
+ partitioning, so unrelated activity can keep a session open.
15
+ - Keep onboarding after user creation and usage after onboarding. Preserve retry
16
+ entries when strict event-count capacity allows. Short windows retain partial
17
+ output and report capacity limits through existing warnings.
18
+ - Reconcile engine-generated identity against surviving auth events, including
19
+ engine-created duplicates and amplification clones. Preserve explicit hook
20
+ overrides and the synthetic experiment identity exception.
21
+ - Select the earliest chronological anchor for append-only path injection.
22
+
23
+ ### Added
24
+
25
+ - Optional `datasetStart` and `datasetEnd` bounds for `applySessionShape`, preventing
26
+ retimed events from being clipped outside a known dataset window. Explicit bounds
27
+ reject impossible session layouts before mutation; calls without bounds retain
28
+ legacy full-day placement.
29
+ - An offline `tests/alignment` family with mixed-dungeon trend checks, neutral
30
+ controls, multiple seeds, sample-size guards, counting contracts, and a diagnostic
31
+ sweep with a hard ten-minute deadline. The runner denies network access at the OS
32
+ level on macOS and fails closed elsewhere.
33
+ - A control inventory, failure-first repair evidence, and measured operating ranges.
34
+ Release validation passed 175 alignment tests and 2,022 unit/integration tests, with
35
+ one existing skip. The 297-cell sweep generated 17.08 million events cumulatively:
36
+ 125 cells met their evidence thresholds and 172 had insufficient evidence.
37
+
38
+ ### Compatibility
39
+
40
+ Existing call forms and defaults remain supported. Session bounds are additive,
41
+ not required. Corrected timestamps, identities, and counts can change generated
42
+ output relative to 1.8.0. Totals counting still requires explicit `reentry: true`
43
+ for repeated histories. These tests use local Mixpanel source-derived contracts,
44
+ not live engine execution, and do not establish complete coverage of every knob.
45
+
46
+ ## 1.8.0 — 2026-09-10
47
+
48
+ ### Added — `standaloneEvents`: identity-less metric snapshots
49
+
50
+ A new top-level config key that generates records describing a **system, not a
51
+ person**. They carry no `user_id` and no `device_id`. Before 1.8.0 the only
52
+ identity-less stream the engine could produce was `$ad_spend` via `hasAdSpend`,
53
+ which is hard-coded to one shape, one cadence, and a Mixpanel reserved event
54
+ name. `standaloneEvents` is the general form.
55
+
56
+ ```js
57
+ standaloneEvents: [{
58
+ event: 'cdn_egress',
59
+ cadence: 'day', // 'hour' | 'day' | 'week', default 'day'
60
+ dimensions: { region: ['us-east', 'us-west', 'eu'] }, // cross-producted
61
+ distinctIdFrom: 'region', // synthetic id, never a person
62
+ properties: {
63
+ gb_out: (ctx) => 400 + ctx.tickIndex * 3,
64
+ cost_usd: (ctx) => (400 + ctx.tickIndex * 3) * 0.085,
65
+ p95_ms: [120, 140, 160],
66
+ },
67
+ }]
68
+ ```
69
+
70
+ - One record per cadence tick per dimension cross-product row.
71
+ - Ticks start at the dataset start and step by the cadence. The last tick is the
72
+ final one at or before the dataset end, so nothing lands in the future.
73
+ - Each record carries `event`, `time`, `insert_id`, `distinct_id`, every
74
+ dimension as a flat property, and every resolved entry in `properties`.
75
+ - `distinct_id` is the value of the dimension named by `distinctIdFrom`, else the
76
+ event name. It exists so Mixpanel accepts the record; it never maps to a person.
77
+ - Property value functions receive a `StandaloneValueContext`:
78
+ `{ time, config, dimensions, tickIndex, tickCount, cadence, event }`.
79
+ `tickIndex / (tickCount - 1)` is window progress — use it to shape a trend.
80
+ - New hook type `standalone` (storage-only). Return the record or an array of
81
+ records; returning nothing drops the record. `meta.spec` carries the resolved
82
+ stream config. The `warehouse` hook instead mutates its row and ignores returns.
83
+ - Lands in `result.standaloneEventData`, writes to a `-STANDALONE` file shard,
84
+ and imports to Mixpanel as its own event stream.
85
+ - Validation **throws** on a malformed entry rather than skipping it. A silent
86
+ skip would drop a whole data stream without the author noticing.
87
+
88
+ New types: `StandaloneEventConfig`, `ResolvedStandaloneEventConfig`,
89
+ `StandaloneValueContext`, `HookMetaStandalone`. `WritePaths` gains
90
+ `standaloneFiles`; `Result` gains `standaloneEventData`; `hookTypes` gains
91
+ `"standalone"`.
92
+
93
+ **Output compatibility.** Additive only. A config without `standaloneEvents` is
94
+ byte-identical to 1.7.0 — the generation pass is gated on the key being present,
95
+ so the seeded RNG stream is untouched. `config.standaloneEvents` normalizes to
96
+ `[]` when absent. Event determinism comparisons exclude the fresh `insert_id`.
97
+
98
+ New tests: `tests/unit/standalone-events.test.js` (25),
99
+ `tests/integration/standalone-events.test.js` (15).
100
+
101
+ ### Added — `warehouseMetrics`: manifest-driven warehouse source tables
102
+
103
+ A new top-level config key that materializes warehouse-ready tables from the
104
+ run's own events after generation completes. This is the local source-table side
105
+ of a warehouse metric demo: bookings rollups, active subscription levels, ARR
106
+ snapshots, and other time-series tables that should read like a real warehouse.
107
+
108
+ ```js
109
+ warehouseMetrics: [{
110
+ name: 'daily_new_bookings',
111
+ source: { event: 'new_booking', measure: 'sum', property: 'booking_value' },
112
+ valueColumn: 'bookings',
113
+ }]
114
+ ```
115
+
116
+ - Supports additive and point-in-time metrics.
117
+ - Grain: `day`, `week`, `month`.
118
+ - Supports subtractive `minus` legs, `groupBy` on up to two declared keys,
119
+ optional `history` backfill, sparse point-in-time emission, seeded `noise`,
120
+ `scale`, and derived `columns`.
121
+ - Lands in `result.warehouseMetricData` keyed by metric name and emits
122
+ `result.warehouseManifest` with table schemas, SQL, and recommended
123
+ aggregation.
124
+ - Writes `<name>-WAREHOUSE-<table>.csv|json` plus
125
+ `<name>-WAREHOUSE-MANIFEST.json` when `writeToDisk` is enabled.
126
+ - Never imports through `token`. Warehouse deploy is a separate flow.
127
+ - New hook type `warehouse` fires once per materialized row with
128
+ `metricName`, `bucketIndex`, `bucketCount`, `grain`, `seriesKey`,
129
+ `isBackfill`, and `raw` bucket stats.
130
+ - Warehouse verification adds `warehouse` / `warehouse-stats` story breakdowns
131
+ plus automatic audits over declared columns, gaps, monotonic time, empty numeric
132
+ cells, and sparse first-bucket coverage. Source correlation is available to story
133
+ assertions; it is not an automatic pass/fail gate.
134
+ - Sparse point-in-time comparison carries emitted levels across missing buckets,
135
+ excludes history from correlation, and compares the first live delta to the scaled
136
+ baseline. Warehouse generation preserves the user event stream, including with noise.
137
+ - Disk verification parses quoted multiline CSV records with `csv-parse` and keeps
138
+ disk and in-memory audit results consistent.
139
+
140
+ ### Added — `/warehouse-metrics`: BigQuery load + warehouse metric save flow
141
+
142
+ The shipped skill at `.claude/skills/warehouse-metrics/` loads the generated
143
+ warehouse tables into BigQuery, connects that dataset to Mixpanel with the
144
+ existing powertools macro, previews each metric SQL, and saves new metrics when
145
+ the CRUD endpoints are available.
146
+
147
+ - Uses the emitted warehouse manifest as the contract.
148
+ - Maps manifest `recommendedAggregation: 'last value'` to the API's
149
+ `aggregation: 'last_value'`.
150
+ - If `GET /crud/getWarehouseMetrics` returns 404, the script still completes the
151
+ BigQuery load and source setup, then writes `warehouse/GAPS.md` for manual
152
+ metric creation.
153
+ - Preview fails fast on the raw substring block (`CREATE`, `UPDATE`, etc.), so
154
+ identifiers like `created_at` and `updated_at` are a real deploy-time trap.
155
+ - The canonical skill name is `/warehouse-metrics`. Bundled commands and handoffs
156
+ use `.claude/skills/warehouse-metrics/`.
157
+ - Uses the shipped Powertools warehouse CRUD and IAM setup macro. Runtime IAM is
158
+ configured; permission failures still stop deployment with the original error.
159
+ - Dry-run works before project provisioning, using placeholders without credentials.
160
+ Live table replacement requires explicit operator consent; the script does not prompt.
161
+
162
+ ### Changed — skill workflow and provisioning context
163
+
164
+ - All nine bundled skills have parsed string argument hints and matching folder names.
165
+ Release tests check frontmatter and warehouse handoffs.
166
+ - Authoring, hooks, and verification distinguish person events, identity-less cadence
167
+ events, and identity-free warehouse rows. Synthetic IDs never count as people.
168
+ - Verification preserves the explicit run prefix and local uncompressed warehouse
169
+ artifacts for deployment. Soup analysis remains scoped to user-event timestamps.
170
+ - Project business context includes separate cadence and warehouse summaries without
171
+ evaluating property functions or including credentials.
172
+ - Headless builds preserve warehouse history and use query preview for fresh results;
173
+ warehouse refresh only invalidates the saved metric cache.
174
+
175
+ ### Added - `/release-check`
176
+
177
+ The new `/release-check` skill audits tests, determinism, documentation, package
178
+ contents, and release handoffs. `.agents/skills` and `.github/skills` link to the
179
+ canonical `.claude/skills` directory for shared agent discovery. Publishing remains
180
+ an explicitly authorized operator action.
181
+
182
+ ### Changed — `streamCSV` preserves falsy cells
183
+
184
+ CSV serialization now writes `0` and `false` as literal cell values instead of
185
+ empty strings. If downstream warehouse SQL or fixtures were treating blank cells
186
+ as zero or false, update them to read the actual value.
187
+
5
188
  ## 1.7.0 — 2026-09-03
6
189
 
7
190
  The engine round for DM4 v5. Executes the 1.6.4 "Deferred to 1.7.0" table plus