@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.
- package/.claude/skills/analyze-soup/SKILL.md +30 -11
- package/.claude/skills/create-dungeon/SKILL.md +84 -44
- package/.claude/skills/create-project/SKILL.md +28 -3
- package/.claude/skills/create-project/context.mjs +89 -0
- package/.claude/skills/create-project/provision.mjs +1 -60
- package/.claude/skills/headless-build/SKILL.md +39 -12
- package/.claude/skills/powertools/SKILL.md +26 -3
- package/.claude/skills/release-check/SKILL.md +124 -0
- package/.claude/skills/verify-dungeon/SKILL.md +103 -29
- package/.claude/skills/verify-dungeon/references/alignment-contract.md +84 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +41 -16
- package/.claude/skills/verify-dungeon/references/report-format.md +41 -10
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +171 -226
- package/.claude/skills/warehouse-metrics/GAPS-template.md +34 -0
- package/.claude/skills/warehouse-metrics/SKILL.md +111 -0
- package/.claude/skills/warehouse-metrics/deploy.mjs +651 -0
- package/.claude/skills/write-hooks/SKILL.md +94 -51
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +165 -18
- package/README.md +265 -1
- package/docs/guides/1.8.0-upgrade-guide.md +151 -0
- package/docs/guides/1.8.1-upgrade-guide.md +153 -0
- package/dungeons/technical/warehouse.js +187 -0
- package/index.js +116 -2
- package/lib/core/config-validator.js +21 -0
- package/lib/core/dungeon-loader.js +1 -1
- package/lib/core/storage.js +51 -3
- package/lib/generators/events.js +6 -0
- package/lib/generators/funnels.js +15 -0
- package/lib/generators/standalone.js +248 -0
- package/lib/generators/warehouse.js +828 -0
- package/lib/hook-helpers/shape.js +73 -17
- package/lib/orchestrators/mixpanel-sender.js +27 -2
- package/lib/orchestrators/user-loop.js +83 -15
- package/lib/templates/story-spec.schema.json +41 -16
- package/lib/utils/utils.js +37 -12
- package/lib/verify/funnel-engine.js +66 -26
- package/lib/verify/index.js +1 -0
- package/lib/verify/story-runner.js +71 -8
- package/lib/verify/warehouse.js +683 -0
- package/package.json +4 -2
- package/scripts/verify-stories.mjs +150 -44
- 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
|
-
|
|
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
|
|
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:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
npm run prune
|
|
170
|
-
```
|
|
186
|
+
## Step 4: Preserve artifacts
|
|
171
187
|
|
|
172
|
-
|
|
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
|
|
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
|
|
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 "
|
|
92
|
+
import * as u from "../../../lib/utils/utils.js";
|
|
91
93
|
import * as v from "ak-tools";
|
|
92
|
-
/** @typedef {import("
|
|
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:
|
|
137
|
-
writeToDisk:
|
|
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`.
|
|
238
|
-
`attempts`
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
298
|
-
|
|
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`.
|
|
304
|
-
|
|
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.
|
|
309
|
-
|
|
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.
|
|
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
|
-
-
|
|
421
|
+
- Usage funnels do not consume this retry plan; model their repetitions through usage selection and volume.
|
|
388
422
|
|
|
389
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
viral=55, decline=5.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
578
|
+
retentionCurve: {
|
|
579
|
+
type: 'logarithmic',
|
|
580
|
+
day1: 0.80,
|
|
581
|
+
day7: 0.50,
|
|
582
|
+
day30: 0.20,
|
|
583
|
+
}
|
|
550
584
|
```
|
|
551
585
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
|
624
|
-
|
|
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,
|
|
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
|
-
##
|
|
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 =
|