@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: analyze-soup
3
3
  description: Use when investigating TimeSoup parameters, diagnosing event-distribution shape, or comparing soup configs — runs a dungeon locally and analyzes time distribution at week/day/hour/minute granularities, producing a soup-analysis.md diagnostic report.
4
- argument-hint: [dungeon path, e.g. dungeons/technical/simplest.js]
4
+ argument-hint: '[dungeon path, e.g. dungeons/technical/simplest.js]'
5
5
  model: claude-opus-4-6
6
6
  effort: max
7
7
  ---
@@ -10,6 +10,12 @@ effort: max
10
10
 
11
11
  Run a dungeon and analyze the time distribution of generated events to evaluate TimeSoup parameters.
12
12
 
13
+ Analyze user `eventData` / EVENTS shards only. `standaloneEvents` runs on a
14
+ fixed cadence before the user loop; `warehouseMetrics` materializes tables
15
+ afterward. Neither measures TimeSoup. Report their cadence or table checks
16
+ separately through `/verify-dungeon`, and preserve warehouse files for
17
+ `/warehouse-metrics`. Never count standalone synthetic ids as people.
18
+
13
19
  **Dungeon file:** `$ARGUMENTS` (default: `dungeons/technical/simplest.js`)
14
20
 
15
21
  ## Step 1: Run the Dungeon
@@ -17,15 +23,20 @@ Run a dungeon and analyze the time distribution of generated events to evaluate
17
23
  Run the dungeon with forced local-only settings:
18
24
 
19
25
  ```bash
20
- npm run prune
21
- node -e "
26
+ node --input-type=module -e "
22
27
  import generate from './index.js';
23
28
  import config from './$ARGUMENTS';
24
- const result = await generate({ ...config, writeToDisk: true, format: 'json', token: '', verbose: true, name: 'soup-analysis' });
29
+ const result = await generate({ ...config, writeToDisk: true, gzip: false, format: 'json', token: '', verbose: true, name: 'soup-analysis' });
25
30
  console.log('Events:', result.eventCount, 'Users:', result.userCount);
26
31
  "
27
32
  ```
28
33
 
34
+ Choose an unused run name before executing; `soup-analysis` below is an example.
35
+ Record the explicit data prefix (`data/soup-analysis`) and use it in every query.
36
+ If that prefix already exists, choose another and update the query paths. Do not
37
+ prune or overwrite an earlier run. Use `<prefix>-EVENTS*.json` with
38
+ `union_by_name=true` for batched output; never glob STANDALONE or WAREHOUSE into it.
39
+
29
40
  Wait for generation to complete. Note the event count and EPS.
30
41
 
31
42
  ## Step 2: Query with DuckDB
@@ -147,7 +158,7 @@ Create `soup-analysis.md` with the structure below. For a user dungeon
147
158
  (`dungeons/user/<name>/<name>.js`) write it into the dungeon's folder
148
159
  (`dungeons/user/<name>/soup-analysis.md`) — everything about a dungeon lives in
149
160
  its folder. Otherwise write it to the project root. (The generated
150
- `./data/soup-analysis-EVENTS.json` is throwaway verification data leave it in
161
+ `./data/soup-analysis-EVENTS.json` is retained verification data; leave it in
151
162
  `./data/`.) Contents:
152
163
 
153
164
  1. **Config**: The soup parameters used (peaks, deviation, mean, numDays)
@@ -158,15 +169,23 @@ its folder. Otherwise write it to the project root. (The generated
158
169
  6. **Assessment**: Overall quality judgment and recommendations
159
170
 
160
171
  ### Quality Criteria
172
+
173
+ These thresholds are diagnostic heuristics, not alignment acceptance criteria.
174
+ Interpret them against the configured macro, soup, resolved window, and intentional
175
+ hook effects. Read the [1.8.1 verification contract](../verify-dungeon/references/alignment-contract.md)
176
+ for proof scope. Report insufficient observations separately from shape failures.
177
+ Include empty buckets and identify partial buckets before computing distribution
178
+ statistics; the SQL above reports observed buckets only. Sparse or partial buckets
179
+ alone do not establish a distribution defect.
180
+
161
181
  - **Daily CV**: 0.2-0.6 is ideal (some variation, not flat or spiky)
162
182
  - **Max-to-avg ratio**: < 2.0 at daily level, < 3.0 at hourly level
163
183
  - **Last day spike**: < 1.5x average = PASS, < 2x = WARN, > 2x = FAIL
164
184
  - **Hourly pattern**: Should show visible peaks but no single hour > 5x average
165
185
 
166
- ## Step 4: Cleanup
167
-
168
- ```bash
169
- npm run prune
170
- ```
186
+ ## Step 4: Preserve artifacts
171
187
 
172
- Remove `soup-analysis.md` only if the user asks. It's meant to persist for comparison across runs.
188
+ Record the prefix and files in the report. Keep warehouse tables and the matching
189
+ manifest until deployment completes. Cleanup requires user consent and an explicit
190
+ list of this run's files. Never run blanket prune. Keep `soup-analysis.md` for
191
+ comparison across runs unless the user asks to remove it.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: create-dungeon
3
- description: Use when authoring a new dungeon-master config from an app description designs events, funnels, properties, identity model, and macro/soup. SCHEMA ONLY; engineered story trends + hooks are added separately by write-hooks.
4
- argument-hint: [free-text app description, e.g. "AI meeting assistant" or "B2B logistics platform"]
3
+ description: Use when authoring a new dungeon-master config from an app description, including standaloneEvents cadence streams and warehouseMetrics source tables. Designs events, funnels, properties, identity model, and macro/soup. SCHEMA ONLY; engineered story trends and hooks are added separately by write-hooks.
4
+ argument-hint: '[free-text app description, e.g. "AI meeting assistant" or "B2B logistics platform"]'
5
5
  model: claude-opus-4-6
6
6
  effort: max
7
7
  ---
@@ -35,11 +35,12 @@ In scope here:
35
35
  - `credentials: { token, region, serviceAccount, serviceSecret, projectId }`
36
36
  - `switches: { hasLocation, hasCampaigns, hasSessionIds, hasAvatar,
37
37
  hasIOSDevices, hasAndroidDevices, hasDesktopDevices, hasBrowser,
38
- isAnonymous, alsoInferFunnels, hasAdSpend, hasAttributionFlags }`
38
+ isAnonymous, alsoInferFunnels, hasAdSpend }`
39
39
  - `identity: { avgDevicePerUser, sessionTimeout }`
40
40
 
41
41
  Old top-level keys keep working (verbose warn nudges migration), but new
42
42
  dungeons should ship the sub-object shape.
43
+ `hasAttributionFlags` is derived from `events[].isAttributionEvent`; do not set it.
43
44
  - Surviving advanced entities: `personas`, `worldEvents`, `engagementDecay`,
44
45
  `dataQuality` — use sparingly
45
46
 
@@ -64,6 +65,7 @@ see `HOOKS.md` at the project root.
64
65
 
65
66
  Before writing any code, scan:
66
67
 
68
+ - [1.8.1 verification contract](../verify-dungeon/references/alignment-contract.md) - report definitions, neutral controls, and proof limits.
67
69
  - `types.d.ts` — the complete API reference. Every Dungeon field, EventConfig
68
70
  flag, Funnel option, AttemptsConfig, and Hook meta interface is documented
69
71
  with full JSDoc. **Treat this as the source of truth.**
@@ -87,9 +89,9 @@ import dayjs from "dayjs";
87
89
  import utc from "dayjs/plugin/utc.js";
88
90
  dayjs.extend(utc);
89
91
  import "dotenv/config";
90
- import * as u from "../../lib/utils/utils.js";
92
+ import * as u from "../../../lib/utils/utils.js";
91
93
  import * as v from "ak-tools";
92
- /** @typedef {import("../../types").Dungeon} Config */
94
+ /** @typedef {import("../../../types").Dungeon} Config */
93
95
 
94
96
  // ── OVERVIEW ──
95
97
  /*
@@ -133,8 +135,8 @@ const config = {
133
135
  numUsers: NUM_USERS,
134
136
  avgEventsPerUserPerDay: EVENTS_PER_DAY,
135
137
  format: "json",
136
- gzip: true,
137
- writeToDisk: false,
138
+ gzip: false,
139
+ writeToDisk: true,
138
140
  concurrency: 1,
139
141
  macro: "flat", // optional — see "Trend shape" below
140
142
  soup: "growth", // optional
@@ -234,8 +236,8 @@ When using experiments, include `$experiment_started` in the events array with
234
236
 
235
237
  - First funnel: includes `isFirstEvent` AND has `isAuthEvent: true` on the
236
238
  identity-transition step.
237
- - Usage funnels: ordinary sequences without `isFirstFunnel`. Optionally use
238
- `attempts` for repeat-usage modeling (abandon-cart pattern).
239
+ - Usage funnels: ordinary sequences without `isFirstFunnel`. Volume and funnel
240
+ selection control repetitions; `attempts` applies only to born-user first funnels.
239
241
  - Pick `conversionRate` between 30 and 80; `timeToConvert` in hours.
240
242
 
241
243
  Funnel `props` stamp constant properties on all events in that funnel run.
@@ -267,9 +269,9 @@ consumed standalone instances as funnel matches. Set `isStrictEvent: false`
267
269
  explicitly only when you intend mixed funnel/standalone semantics for that
268
270
  event.
269
271
 
270
- **Mark hook-readable funnel-step events with `isStrictEvent: false`.** When a hook reads `event === 'X'` and `X` is also a funnel step, the validator's auto-promote turns it into `isStrictEvent: true` and the engine stops emitting standalone occurrences — cohort goes empty. Identify these candidates at schema time so `write-hooks` doesn't have to re-thread the schema. Common candidates: login, page view, search, add to cart, swap, deposit — anything that's both a funnel step AND a recurring user behavior hooks will likely cohort on.
272
+ **Use `isStrictEvent: false` when a cohort needs standalone occurrences.** Auto-promotion removes standalone traffic but leaves funnel-generated occurrences readable. Declare the opt-out at schema time only when that extra traffic is part of the intended behavior; a hook reading an event name alone does not require it.
271
273
 
272
- **Funnels representing loops need `reentry: true`.** Any funnel named "X loop" / "X cycle" / "session" / per-instance recurring behavior must declare `reentry: true`. Without it, the engine emits one sequence per user and downstream "power user" / "daily active" cohorts have no behavioral signal to bin on.
274
+ **`reentry` is verifier-only.** Usage volume, funnel selection, and the event budget control generated repetitions. Set `reentry: true` only when the intended report counts repeated histories. It does not generate loops; local totals also default to `reentry: false`.
273
275
 
274
276
  **Structural trend engineering — duplicate funnels instead of reaching for
275
277
  hooks.** Not every trend needs a hook: initial conditions can raise or lower
@@ -294,20 +296,19 @@ to architect a comparison directly into the schema:
294
296
  Prefer structure when the story is a *between-path comparison* (this path
295
297
  converts worse / slower than that one). Reach for hooks when the story is a
296
298
  *within-cohort behavior* (these users do more of X over time, this segment's
297
- values differ). Structural trends are cheaper to verify — the knob IS the
298
- expected value.
299
+ values differ). Structural trends still need report-level proof: competing
300
+ histories, saturation, and finite budgets can change the observed effect.
299
301
 
300
302
  ### 3. SuperProps (2–3)
301
303
 
302
304
  Properties present on EVERY event. Common picks: `Plan`, `Region`, `Platform`,
303
- `App Version`. Values must come from same enumerations used in `userProps` for
304
- consistency.
305
+ `App Version`. Matching `userProps` enumerations only shares a value domain.
306
+ Use `stickyEventProps` for values that must match each user's profile.
305
307
 
306
308
  ### 4. UserProps (4–8)
307
309
 
308
- User profile properties. Set once per user. Use enumerations whose values
309
- match `superProps` for any overlapping keys (so per-event Region matches per-user
310
- Region).
310
+ User profile properties. Set once per user. Declare overlapping event keys in
311
+ `superProps` and use `stickyEventProps` when profile/event equality is required.
311
312
 
312
313
  ### 5. Groups (0–2)
313
314
 
@@ -319,7 +320,40 @@ group attributes. Skip for B2C apps.
319
320
  Slowly-changing dimensions for plan tier, role, etc. JSDoc on `SCDProp` covers
320
321
  type/frequency/timing/values/max.
321
322
 
322
- ### 7. Hook function DO NOT WRITE
323
+ ### 7. Metric data surfaces (v1.8.0)
324
+
325
+ Author these schemas here when the app needs system telemetry or warehouse tables.
326
+ Requests that call these surfaces "v2" still target v1.8.0; do not bump the version.
327
+
328
+ | Surface | Required design | Time settings | Output |
329
+ |---|---|---|---|
330
+ | `standaloneEvents` | `event`, declared `dimensions` and `properties`; optional `distinctIdFrom` naming a dimension | `cadence: 'hour'`, `'day'`, or `'week'` | `result.standaloneEventData`, `-STANDALONE*.json`; imports as events |
331
+ | `warehouseMetrics` | `name`, `type: 'additive'` or `'point-in-time'`, `source`; declare extra `columns` | `grain: 'day'`, `'week'`, or `'month'`, optional `history` bucket count | `result.warehouseMetricData`, `-WAREHOUSE-*` tables and `-WAREHOUSE-MANIFEST.json`; separate deploy |
332
+
333
+ `standaloneEvents` generates before the user loop. Each cadence tick emits one
334
+ record per dimension cross-product row. Its synthetic `distinct_id` identifies a
335
+ series, never a person. Do not put these events in funnels, retention, identity
336
+ stitch checks, or people counts. They do not inherit user metadata or superProps.
337
+
338
+ Standalone property functions receive `{ time, config, dimensions, tickIndex,
339
+ tickCount, cadence, event }`, with `time` in milliseconds. Guard `tickCount <= 1`
340
+ before dividing by `tickCount - 1`. Declare every property and dimension here.
341
+
342
+ Warehouse sources reference only user `events[]`, including names in both
343
+ `source.event` (plus) and optional `source.minus`. A `standaloneEvents` stream
344
+ cannot be a warehouse source. Declare `source.property` and every `source.groupBy`
345
+ key on every plus and minus event, or in `superProps`. `sum` and `avg` require
346
+ `source.property`; point-in-time disallows `avg` and `dau`. Use at most two group
347
+ keys. `sparse: true` applies only to point-in-time metrics.
348
+
349
+ Warehouse materialization runs after the user loop. Extra column functions receive
350
+ `{ value, row, time, bucketIndex, bucketCount, grain, isBackfill, seriesKey, spec,
351
+ config }`. `time` is milliseconds. `history` adds synthetic pre-window buckets;
352
+ it does not create historical user events. Keep `timeColumn`, `valueColumn`, group
353
+ keys, and extra `columns` distinct and declared. Read `dungeons/technical/warehouse.js`
354
+ and the warehouse interfaces in `types.d.ts` before choosing measures or backfill.
355
+
356
+ ### 8. Hook function — DO NOT WRITE
323
357
 
324
358
  Skip the `hook:` field entirely (engine defaults to pass-through). The
325
359
  `write-hooks` skill picks this up and engineers the trends.
@@ -384,9 +418,9 @@ Typical ranges:
384
418
 
385
419
  - Direct-acquisition first funnels: `{ min: 0, max: 0 }` (single attempt) or omit
386
420
  - Shared-link / friction-heavy onboarding: `{ min: 0, max: 2 }` (some retry)
387
- - Re-engagement / abandon-cart usage funnels: `{ min: 0, max: 3 }`
421
+ - Usage funnels do not consume this retry plan; model their repetitions through usage selection and volume.
388
422
 
389
- When set, `attempts.conversionRate` (optional) overrides `funnel.conversionRate`
423
+ For a born user's first funnel, `attempts.conversionRate` (optional) overrides `funnel.conversionRate`
390
424
  on the FINAL attempt. Failed prior attempts truncate before the first
391
425
  `isAuthEvent` step (no stitch fires for those attempts).
392
426
 
@@ -471,12 +505,12 @@ intra-day rhythm). Only override when you have a specific reason:
471
505
 
472
506
  ### Macro × born% / bias compatibility (strict clamps)
473
507
 
474
- When you set `macro` AND `percentUsersBornInDataset` explicitly, the validator
475
- clamps born% to the macro's preset value: flat=12, steady=12, growth=30,
476
- viral=55, decline=5. Same for `bornRecentBias` outside `[-0.5, 0.5]`. If you
477
- need higher born% (e.g., "this app launched mid-window every user is in the
478
- dataset"), switch macros first (flat growth viral) instead of pushing the
479
- preset's value. Setting born% without a macro keeps legacy behavior (no clamp).
508
+ The per-preset born% cap applies only to a named preset (`macro: 'growth'` or
509
+ `macro: { preset: 'growth', ... }`) with explicit `percentUsersBornInDataset`:
510
+ flat=12, steady=12, growth=30, viral=55, decline=5. A custom macro object without
511
+ `preset` has no per-preset cap; own and verify its shape. The global born% clamp
512
+ to [0,100] still applies. Explicit `bornRecentBias` is separately clamped to
513
+ [-0.5,0.5], with compound protection for high born% and bias. Preset bias is exempt.
480
514
 
481
515
  The clamp warning explains why and points to safe alternatives — read it.
482
516
 
@@ -541,18 +575,19 @@ Top-level optional knob. Shape retention via log-linear interpolation
541
575
  between waypoints. Independent of `engagementDecay`.
542
576
 
543
577
  ```js
544
- retentionCurve: [
545
- { day: 0, retention: 1.0 },
546
- { day: 1, retention: 0.80 },
547
- { day: 7, retention: 0.50 },
548
- { day: 30, retention: 0.20 },
549
- ]
578
+ retentionCurve: {
579
+ type: 'logarithmic',
580
+ day1: 0.80,
581
+ day7: 0.50,
582
+ day30: 0.20,
583
+ }
550
584
  ```
551
585
 
552
- Each born-in-dataset user's events get filtered based on the interpolated
553
- retention at the event's age-from-first-event-day. Use when you want a
554
- declarative retention shape at config level (analytical-style D1/D7/D30
555
- targets) instead of writing hook logic.
586
+ Use `type: 'logarithmic'` (default) or `'linear'`, with `dayN` active-day
587
+ weights; day 0 is implicitly 1. The curve weights the active-day plan and takes
588
+ precedence over `avgActiveDaysPerUser`. It does not directly filter each event
589
+ or guarantee report D1/D7/D30 percentages. Verify mature cohorts and account for
590
+ funnel spill and finite event budgets.
556
591
 
557
592
  ### `userSeed` (separate distinct_id RNG seed, v1.5+)
558
593
 
@@ -568,16 +603,15 @@ userSeed: "users-v1", // user-pool RNG (stable across versions)
568
603
 
569
604
  ## SuperProp consistency rule
570
605
 
571
- If `superProps` and `userProps` both define a property like `Plan`, the
572
- enumeration must match exactly. Otherwise users with `userProps.Plan = 'pro'`
573
- will fire events with `superProps.Plan = 'free'` — Mixpanel will see broken
574
- breakdowns.
606
+ Matching enumerations do not guarantee profile/event equality: independent draws
607
+ can differ. Declare both keys and project the profile values with `stickyEventProps`.
575
608
 
576
609
  ```js
577
610
  const PLANS = ["Free", "Free", "Free", "Pro", "Pro", "Enterprise"];
578
611
  // ...
579
612
  superProps: { Plan: PLANS, Region: REGIONS },
580
613
  userProps: { Plan: PLANS, Region: REGIONS, Role: ROLES, ... },
614
+ stickyEventProps: ['Plan', 'Region'],
581
615
  ```
582
616
 
583
617
  ## Verification
@@ -620,10 +654,16 @@ This keeps `dungeons/user/` organized — EVERYTHING about this dungeon lives in
620
654
  the same folder: `hook-results.md` + `hook-query-log.txt` +
621
655
  `<name>-verifications.sql` (from `verify-dungeon`), `soup-analysis.md` (from
622
656
  `analyze-soup`), briefs, schema CSV/JSON, example data. The only thing kept
623
- outside is the throwaway verification data the runs write to `./data/` (cleaned
624
- after).
657
+ outside is the verification data the runs write to `./data/`. Preserve the exact
658
+ run prefix and its files until verification and warehouse deployment are complete.
625
659
 
626
660
  Do NOT inject hooks. Do NOT use `subscription`, `attribution`, `geo`,
627
661
  `features`, or `anomalies` (the engine will silently strip them and warn).
628
662
 
629
- When done, tell the user the next skill to run.
663
+ When done, hand off to `/write-hooks` when trends are needed, then `/verify-dungeon`.
664
+ For artifact generation use local `writeToDisk: true`, `format: 'json'`,
665
+ `gzip: false`, and an explicit unique run name. Verification runs must disable
666
+ sending with a top-level `token: ''` override. Provision with `/create-project`
667
+ after verification. If `warehouseMetrics` exists, route to `/warehouse-metrics`
668
+ with the verified disk artifact prefix after project provisioning. Keep those
669
+ files; ordinary event import does not deploy warehouse tables.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: create-project
3
3
  description: Use when an existing dungeon needs a real Mixpanel project provisioned before sending data — creates the project, sets timezone UTC, mints a scoped service account, adds the dungeon's group keys, uploads business context (AI context), and writes the resulting credentials back into the dungeon so it "just runs". Follows create-dungeon / write-hooks / verify-dungeon.
4
- argument-hint: [dungeon path, e.g. dungeons/user/shopstream/shopstream.js]
4
+ argument-hint: '[dungeon path, e.g. dungeons/user/shopstream/shopstream.js]'
5
5
  model: claude-opus-4-6
6
6
  effort: max
7
7
  ---
@@ -16,6 +16,12 @@ This is the step after a dungeon is authored, reviewed, and tweaked. It turns a
16
16
  local dungeon into one you can actually send to Mixpanel by creating the project
17
17
  and stamping `credentials` back into the file.
18
18
 
19
+ Follow the [1.8.1 verification contract](../verify-dungeon/references/alignment-contract.md)
20
+ when carrying verification claims into business context. Preserve the actual report
21
+ definition and explicit counting options. Local evidence is selected source-derived
22
+ verification, not live Mixpanel parity. Provisioning is a separately authorized
23
+ handoff; never run it automatically during offline verification.
24
+
19
25
  ## What it does
20
26
 
21
27
  All work runs through the orchestrator `provision.mjs` (this skill's directory),
@@ -25,7 +31,7 @@ in order:
25
31
  1. **createProject** — name derived from the dungeon's `OVERVIEW` (`NAME:` line), region `US`, timezone `UTC` (set as a follow-up by the endpoint).
26
32
  2. **mintServiceAccount** — `admin`, expires `+30 days`, scoped to the new project. This is what the dungeon uses to **send** data.
27
33
  3. **addGroupKey** — one per `groupKeys` entry in the dungeon (`property_name` + a titleized `display_name`). Skipped if the dungeon has no group keys.
28
- 4. **setBusinessContext** — markdown built from the dungeon's `OVERVIEW` comment block plus the `stories` named export (per story: `narrative` + `mixpanelReport` + intentional deviations); dungeons without stories fall back to the `HOOK STORIES` comment scrape (via the package's `extractComments`). Plus an events/funnels/props/group-keys summary, capped at 50k chars. The dry-run plan prints which source was used.
34
+ 4. **setBusinessContext** — markdown built from the dungeon's `OVERVIEW` comment block plus the `stories` named export (per story: `narrative` + `mixpanelReport` + intentional deviations); dungeons without stories fall back to the `HOOK STORIES` comment scrape (via the package's `extractComments`). Plus an events/funnels/props/group-keys summary and separate standalone-event and warehouse-metric summaries, capped at 50k chars. The dry-run plan prints which source was used.
29
35
  5. **write-back** — replaces the dungeon's `credentials: { … }` block with `{ token, projectId, serviceAccount, serviceSecret, region }`.
30
36
 
31
37
  **Always creates a fresh project.** Re-running mints a new project and overwrites
@@ -83,7 +89,26 @@ run command:
83
89
  node scripts/run-dungeon.mjs <dungeon-path>
84
90
  ```
85
91
 
86
- ## Error handling
92
+ ## Metric handoff (v1.8.0)
93
+
94
+ Verify `standaloneEvents` and `warehouseMetrics` locally before provisioning.
95
+ Keep the verified run's disk files and record its explicit prefix. Cadence events
96
+ import through the normal send path; their synthetic `distinct_id` values must
97
+ never become people counts or identity-model evidence.
98
+
99
+ After credentials are written, route `warehouseMetrics` to `/warehouse-metrics`
100
+ with that prefix and `credentials.projectId`. Require local uncompressed table
101
+ files (`writeToDisk: true`, `gzip: false`) and the matching warehouse manifest.
102
+ Review the deploy dry run before obtaining consent for live writes. Project
103
+ provisioning alone does not load BigQuery tables or save warehouse metrics.
104
+
105
+ The pure `context.mjs` helper summarizes cadence, dimension keys and cardinalities,
106
+ property names, and synthetic identity separately from person events. Warehouse
107
+ summaries include sources, measures, grain, type, point-in-time baseline, history,
108
+ dimension and output columns, aggregation, and the separate deployment handoff.
109
+ It does not evaluate property functions or include config credentials.
110
+
111
+ ## Provisioning errors
87
112
 
88
113
  - **Missing `BEARER_TOKEN` / `ORG_ID`** — orchestrator exits; have the user fix `.env`.
89
114
  - **`createProject` fails** — nothing is provisioned; surface the power-tools error (`{ error }` or `{ errors:[{param,message}] }`) verbatim and stop.
@@ -0,0 +1,89 @@
1
+ export function buildContext(name, config, comments, groupKeys, stories) {
2
+ const parts = [`# ${name}`, ''];
3
+ if (comments.overview) parts.push(comments.overview, '');
4
+
5
+ if (stories?.length) {
6
+ parts.push('## Engineered Behaviors', '');
7
+ parts.push(`${stories.length} machine-verified story patterns (from the dungeon's \`stories\` export):`, '');
8
+ for (const s of stories) {
9
+ const arch = s.archetype ? ` — ${s.archetype}` : '';
10
+ parts.push(`### ${s.id}${arch}`, '');
11
+ if (s.narrative) parts.push(String(s.narrative).trim(), '');
12
+ if (s.mixpanelReport && typeof s.mixpanelReport === 'object') {
13
+ parts.push('How to see it in Mixpanel:');
14
+ for (const [k, v] of Object.entries(s.mixpanelReport)) {
15
+ parts.push(`- **${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
16
+ }
17
+ parts.push('');
18
+ }
19
+ if (Array.isArray(s.intentionalDeviations) && s.intentionalDeviations.length) {
20
+ parts.push('Notes:', ...s.intentionalDeviations.map((d) => `- ${d}`), '');
21
+ }
22
+ }
23
+ } else if (comments.hookStories) {
24
+ parts.push('## Engineered Behaviors', '', comments.hookStories, '');
25
+ }
26
+
27
+ parts.push('## Schema', '');
28
+ const events = config.events || [];
29
+ parts.push(`### Events (${events.length})`);
30
+ for (const e of events) {
31
+ const props = e.properties ? Object.keys(e.properties).join(', ') : '';
32
+ const weight = e.weight != null ? ` (weight ${e.weight})` : '';
33
+ parts.push(`- ${e.event}${weight}${props ? ` — ${props}` : ''}`);
34
+ }
35
+ parts.push('');
36
+
37
+ const funnels = config.funnels || [];
38
+ if (funnels.length) {
39
+ parts.push(`### Funnels (${funnels.length})`);
40
+ for (const f of funnels) {
41
+ const seq = (f.sequence || []).join(' → ');
42
+ const rate = f.conversionRate != null ? ` (${f.conversionRate}%)` : '';
43
+ parts.push(`- ${f.name || '(unnamed)'}: ${seq}${rate}`);
44
+ }
45
+ parts.push('');
46
+ }
47
+
48
+ if (groupKeys.length) {
49
+ parts.push('### Group keys', ...groupKeys.map((g) => `- ${g.property_name} (${g.display_name})`), '');
50
+ }
51
+
52
+ const standaloneEvents = config.standaloneEvents || [];
53
+ if (standaloneEvents.length) {
54
+ parts.push(`### Standalone events (${standaloneEvents.length})`);
55
+ parts.push('Cadence streams use the normal event send path. Their synthetic distinct_id values are never people counts or identity-model evidence.', '');
56
+ for (const stream of standaloneEvents) {
57
+ const dimensions = Object.entries(stream.dimensions || {}).map(([key, values]) => `${key} (${values.length} values)`);
58
+ const properties = Object.keys(stream.properties || {});
59
+ const identity = stream.distinctIdFrom ? `dimension ${stream.distinctIdFrom}` : 'event name';
60
+ parts.push(`- ${stream.event}: cadence ${stream.cadence ?? 'day'}; dimensions ${contextList(dimensions)}; properties ${contextList(properties)}; synthetic distinct_id from ${identity}`);
61
+ }
62
+ parts.push('');
63
+ }
64
+
65
+ const warehouseMetrics = config.warehouseMetrics || [];
66
+ if (warehouseMetrics.length) {
67
+ parts.push(`### Warehouse metrics (${warehouseMetrics.length})`);
68
+ parts.push('Sum adds buckets; LastValue reads the latest snapshot.', '');
69
+ parts.push('Separate /warehouse-metrics deployment is required; provisioning and sendToMixpanel do not load warehouse tables or save warehouse metrics.', '');
70
+ for (const metric of warehouseMetrics) {
71
+ const type = metric.type ?? 'additive';
72
+ const source = metric.source;
73
+ const aggregation = type === 'point-in-time' ? 'LastValue' : 'Sum';
74
+ parts.push(`- ${metric.name}: type ${type}; grain ${metric.grain ?? 'day'}; history ${metric.history ?? 0} periods before the event window; sparse ${metric.sparse ?? false}; Mixpanel aggregation ${aggregation}`);
75
+ if (type === 'point-in-time') parts.push(` baseline ${metric.baseline ?? 0} (point-in-time starting level)`);
76
+ parts.push(` source events ${contextList(source.event)}; minus ${contextList(source.minus)}; measure ${source.measure ?? 'count'}; property ${source.property ?? '(none)'}; where ${source.where ? 'present (function, not evaluated)' : '(none)'}`);
77
+ parts.push(` columns: time ${metric.timeColumn ?? 'date'}; value ${metric.valueColumn ?? 'value'}; groupBy ${contextList(source.groupBy)}; extra ${contextList(Object.keys(metric.columns || {}))}`);
78
+ }
79
+ parts.push('');
80
+ }
81
+
82
+ let md = parts.join('\n');
83
+ if (md.length > 50000) md = md.slice(0, 49900) + '\n\n…(truncated to 50,000 chars)';
84
+ return md;
85
+ }
86
+
87
+ function contextList(value) {
88
+ return (Array.isArray(value) ? value.join(', ') : value) || '(none)';
89
+ }
@@ -32,6 +32,7 @@ import path, { dirname, resolve } from 'path';
32
32
  import { fileURLToPath, pathToFileURL } from 'url';
33
33
  import dotenv from 'dotenv';
34
34
  import { loadFromFile, extractComments } from '../../../index.js';
35
+ import { buildContext } from './context.mjs';
35
36
 
36
37
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
38
  const REPO_ROOT = resolve(__dirname, '../../../');
@@ -204,66 +205,6 @@ function isoInDays(days) {
204
205
  return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
205
206
  }
206
207
 
207
- function buildContext(name, config, comments, groupKeys, stories) {
208
- const parts = [`# ${name}`, ''];
209
- if (comments.overview) parts.push(comments.overview, '');
210
-
211
- // Preferred source: the stories export — each story's narrative is the
212
- // human-readable behavior and mixpanelReport (free-form object) points at
213
- // the report where it shows. Comment scrape is the fallback for dungeons
214
- // without stories (schema-only or pre-1.6).
215
- if (stories?.length) {
216
- parts.push('## Engineered Behaviors', '');
217
- parts.push(`${stories.length} machine-verified story patterns (from the dungeon's \`stories\` export):`, '');
218
- for (const s of stories) {
219
- const arch = s.archetype ? ` — ${s.archetype}` : '';
220
- parts.push(`### ${s.id}${arch}`, '');
221
- if (s.narrative) parts.push(String(s.narrative).trim(), '');
222
- if (s.mixpanelReport && typeof s.mixpanelReport === 'object') {
223
- parts.push('How to see it in Mixpanel:');
224
- for (const [k, v] of Object.entries(s.mixpanelReport)) {
225
- parts.push(`- **${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
226
- }
227
- parts.push('');
228
- }
229
- if (Array.isArray(s.intentionalDeviations) && s.intentionalDeviations.length) {
230
- parts.push('Notes:', ...s.intentionalDeviations.map((d) => `- ${d}`), '');
231
- }
232
- }
233
- } else if (comments.hookStories) {
234
- parts.push('## Engineered Behaviors', '', comments.hookStories, '');
235
- }
236
-
237
- parts.push('## Schema', '');
238
- const events = config.events || [];
239
- parts.push(`### Events (${events.length})`);
240
- for (const e of events) {
241
- const props = e.properties ? Object.keys(e.properties).join(', ') : '';
242
- const weight = e.weight != null ? ` (weight ${e.weight})` : '';
243
- parts.push(`- ${e.event}${weight}${props ? ` — ${props}` : ''}`);
244
- }
245
- parts.push('');
246
-
247
- const funnels = config.funnels || [];
248
- if (funnels.length) {
249
- parts.push(`### Funnels (${funnels.length})`);
250
- for (const f of funnels) {
251
- const seq = (f.sequence || []).join(' → ');
252
- const rate = f.conversionRate != null ? ` (${f.conversionRate}%)` : '';
253
- parts.push(`- ${f.name || '(unnamed)'}: ${seq}${rate}`);
254
- }
255
- parts.push('');
256
- }
257
-
258
- if (groupKeys.length) {
259
- parts.push('### Group keys', ...groupKeys.map((g) => `- ${g.property_name} (${g.display_name})`), '');
260
- }
261
-
262
- let md = parts.join('\n');
263
- if (md.length > 50000) md = md.slice(0, 49900) + '\n\n…(truncated to 50,000 chars)';
264
- return md;
265
- }
266
-
267
208
  function writeBackCredentials(p, creds) {
268
209
  let src = readFileSync(p, 'utf-8');
269
210
  const block =