@ak--47/dungeon-master 1.8.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 +9 -0
- package/.claude/skills/create-dungeon/SKILL.md +35 -34
- package/.claude/skills/create-project/SKILL.md +6 -0
- package/.claude/skills/headless-build/SKILL.md +21 -11
- package/.claude/skills/powertools/SKILL.md +6 -2
- package/.claude/skills/release-check/SKILL.md +27 -2
- package/.claude/skills/verify-dungeon/SKILL.md +32 -13
- package/.claude/skills/verify-dungeon/references/alignment-contract.md +84 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +27 -16
- package/.claude/skills/verify-dungeon/references/report-format.md +23 -9
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +135 -225
- package/.claude/skills/warehouse-metrics/SKILL.md +6 -0
- package/.claude/skills/write-hooks/SKILL.md +61 -48
- package/CHANGELOG.md +41 -0
- package/HOOKS.md +60 -13
- package/README.md +37 -1
- package/docs/guides/1.8.1-upgrade-guide.md +153 -0
- package/lib/generators/events.js +6 -0
- package/lib/generators/funnels.js +15 -0
- package/lib/hook-helpers/shape.js +73 -17
- package/lib/orchestrators/user-loop.js +82 -15
- package/lib/verify/funnel-engine.js +66 -26
- package/package.json +1 -1
- package/types.d.ts +9 -5
|
@@ -28,8 +28,8 @@ In scope:
|
|
|
28
28
|
|
|
29
29
|
Out of scope:
|
|
30
30
|
- Schema changes (events, properties, funnels, superProps, userProps).
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
44
|
-
|
|
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
|
|
@@ -71,11 +72,11 @@ Hooks fire in this order per user (see `CLAUDE.md` for the canonical reference):
|
|
|
71
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
|
-
|
|
75
|
-
|
|
76
|
-
the
|
|
77
|
-
|
|
78
|
-
`props
|
|
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.
|
|
79
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
|
|
|
@@ -186,7 +187,7 @@ if (type === 'funnel-post' && meta.experiment) {
|
|
|
186
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)`) |
|
|
187
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 |
|
|
188
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 |
|
|
189
|
-
| shape | `applySessionShape(events, uid, opts)` | Retime the
|
|
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. |
|
|
190
191
|
|
|
191
192
|
### Hook anti-patterns
|
|
192
193
|
|
|
@@ -236,8 +237,9 @@ writing a custom hook:
|
|
|
236
237
|
No special hook needed; engineer cohort behavior via `engagementDecay`,
|
|
237
238
|
`dropEventsWhere`, or per-user filtering in `everything`.
|
|
238
239
|
- **Session metrics** ("avg session has 6 events, lasts 4 minutes") — verify
|
|
239
|
-
with `emulateBreakdown({ type: 'sessionMetrics' })`.
|
|
240
|
-
`session_id
|
|
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.
|
|
241
243
|
- **Reentry funnels** ("power users complete the funnel 3+ times") — set
|
|
242
244
|
`Funnel.reentry: true` (verifier hint). Engineer multiple completions via
|
|
243
245
|
`funnel-post` injecting cloned funnel sequences for that cohort.
|
|
@@ -254,9 +256,10 @@ writing a custom hook:
|
|
|
254
256
|
breakdown with `timeBucket: 'week'`. Engineer via temporal-windowed hooks
|
|
255
257
|
using `DATASET_START.add(N, 'days')`.
|
|
256
258
|
- **Identity-model dungeons** — when `identity.avgDevicePerUser > 0`
|
|
257
|
-
(or the deprecated `hasAnonIds: true`),
|
|
258
|
-
|
|
259
|
-
events
|
|
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.
|
|
260
263
|
|
|
261
264
|
**Schema-first reminder:** exclusion events must be declared in `events[]`
|
|
262
265
|
before referencing them as `Funnel.exclusionEvents` — the validator throws
|
|
@@ -270,9 +273,9 @@ emulator can re-derive.
|
|
|
270
273
|
| Pattern | Mixpanel analysis | Hook type | Caveat (HOOKS.md) |
|
|
271
274
|
|---------|-------------------|-----------|-------------------|
|
|
272
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 |
|
|
273
|
-
| `applyFunnelFrequencyBreakdown` | Funnels
|
|
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. |
|
|
274
277
|
| `applyAggregateByBin` | Insights — avg(prop X) by per-user count(B) | everything | Same `binBy: 'distinctDays'` default as above |
|
|
275
|
-
| `applyTTCBySegmentV2` | Funnel TTC
|
|
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. |
|
|
276
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) |
|
|
277
280
|
|
|
278
281
|
Use a pattern when the trend matches its analysis 1:1. Drop down to atoms when
|
|
@@ -298,13 +301,17 @@ non-negotiable design rule learned the hard way:
|
|
|
298
301
|
visualization even though the data is there. Label-only path reads can
|
|
299
302
|
INVERT when a busier cohort glues extra visible events between path steps —
|
|
300
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.
|
|
301
306
|
- **`session-shape`** (`applySessionShape`) — 30-MIN STRADDLING + MIDNIGHT
|
|
302
|
-
RULE.
|
|
303
|
-
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
|
|
304
309
|
UNDER 30min and inter-session gaps clearly OVER it — a gap that straddles
|
|
305
310
|
the timeout makes session counts jitter across runs. Never let an
|
|
306
311
|
engineered session cross UTC midnight (the day split cuts it in two). The
|
|
307
|
-
|
|
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.
|
|
308
315
|
- **`composition-drift`** — the breakdown's SHARE of a segment moves over
|
|
309
316
|
time while totals stay flat (e.g. plan-mix shifts toward premium). Engineer
|
|
310
317
|
by flipping an existing property value on a date-gated cohort, never by
|
|
@@ -339,9 +346,9 @@ events.push(clone); // ✅ inject from template
|
|
|
339
346
|
return events.filter(e => !shouldDrop(e)); // ✅ filter inside `everything`
|
|
340
347
|
```
|
|
341
348
|
|
|
342
|
-
If a trend
|
|
343
|
-
|
|
344
|
-
|
|
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.
|
|
345
352
|
|
|
346
353
|
## Identity-aware hook patterns
|
|
347
354
|
|
|
@@ -463,8 +470,9 @@ Rules:
|
|
|
463
470
|
- `floor` must itself be derived (e.g. `target * 0.8`) — never hand-tuned to
|
|
464
471
|
a run. A missed assertion means fixing the hook or the derivation, never
|
|
465
472
|
relaxing the number to match output.
|
|
466
|
-
- Set `minCohort` from the
|
|
467
|
-
|
|
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.
|
|
468
476
|
- One story per pattern; story `hook` matches the doc-block numbering (`H3`).
|
|
469
477
|
- The `assert` function escape hatch is discouraged — each use needs a
|
|
470
478
|
comment saying why the declarative `expect` grammar can't express it.
|
|
@@ -474,12 +482,12 @@ Rules:
|
|
|
474
482
|
|
|
475
483
|
The order of operations inside the everything hook matters when hooks interact:
|
|
476
484
|
|
|
477
|
-
1. **
|
|
485
|
+
1. **Profile projection** - prefer schema-declared `stickyEventProps`; preserve projected values in clones
|
|
478
486
|
2. **Temporal value mutations that DON'T need cloned events** — e.g., version stamping
|
|
479
487
|
3. **Behavioral detection + event cloning** — agentic detection, KYC clones, pro clones, magic number clones
|
|
480
488
|
4. **Event filtering** — churn, retention, rate-limit drops
|
|
481
|
-
5. **Temporal value mutations that NEED cloned events**
|
|
482
|
-
6. **
|
|
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
|
|
483
491
|
|
|
484
492
|
**Why:** If a temporal mutation runs before cloning, cloned events that land in
|
|
485
493
|
the temporal window miss the mutation. Moving temporal value mutations to the
|
|
@@ -489,7 +497,8 @@ end ensures ALL events in the window — original and cloned — receive the eff
|
|
|
489
497
|
|
|
490
498
|
When a dungeon relied on deprecated config blocks (`subscription`, `attribution`,
|
|
491
499
|
`features`, `geo`, `anomalies`) for properties that hooks depend on, those
|
|
492
|
-
properties no longer appear in the data.
|
|
500
|
+
properties no longer appear in the data. Request an authorized schema migration
|
|
501
|
+
through `/create-dungeon` first:
|
|
493
502
|
|
|
494
503
|
1. Add the property to `superProps` and `userProps` with default values
|
|
495
504
|
2. Assign meaningful values in the `user` hook (based on hash, persona, or profile)
|
|
@@ -506,8 +515,8 @@ control group, but not so broad they catch everyone:
|
|
|
506
515
|
| Detection | Problem | Fix |
|
|
507
516
|
|-----------|---------|-----|
|
|
508
517
|
| `events.some(e => e.event === X)` with common X | 90%+ of users qualify | Require 3+ events: `events.filter(...).length >= 3` |
|
|
509
|
-
| `charCodeAt(0) % 50 === 0` |
|
|
510
|
-
| `profile.tier === "premium"` | Fixed by config distribution |
|
|
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 |
|
|
511
520
|
| `earlyEvents.length >= 5` for a low-weight event | 0% qualify (impossible threshold) | Check actual distribution first, set at ~80th percentile |
|
|
512
521
|
|
|
513
522
|
Target: 10-30% of users in the affected cohort for clean signal at 10K users.
|
|
@@ -546,11 +555,12 @@ Reserve drops for a single churn/retention effect per dungeon.
|
|
|
546
555
|
|
|
547
556
|
Apply these BEFORE handing off to `/verify-dungeon`. See HOOKS.md §9 for full recipes.
|
|
548
557
|
|
|
549
|
-
###
|
|
558
|
+
### Opt out of strict events only when standalone occurrences are required
|
|
550
559
|
|
|
551
560
|
If your hook reads `event === 'X'` and `X` is also a funnel-step event, the
|
|
552
561
|
validator auto-promotes it to `isStrictEvent: true` and the engine
|
|
553
|
-
won't emit standalone occurrences.
|
|
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.
|
|
554
564
|
|
|
555
565
|
```js
|
|
556
566
|
// BAD — login is a funnel step + read by hook
|
|
@@ -559,16 +569,15 @@ events: [{ event: 'login', weight: 4, properties: {...} }]
|
|
|
559
569
|
events: [{ event: 'login', weight: 4, isStrictEvent: false, properties: {...} }]
|
|
560
570
|
```
|
|
561
571
|
|
|
562
|
-
Audit
|
|
563
|
-
|
|
572
|
+
Audit whether the cohort needs standalone occurrences. Request any required
|
|
573
|
+
`isStrictEvent: false` schema edit through `/create-dungeon`; do not change it here.
|
|
564
574
|
|
|
565
575
|
### Reentry on per-instance loops
|
|
566
576
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
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.
|
|
572
581
|
|
|
573
582
|
### Hash-based cohorts produce textbook signals
|
|
574
583
|
|
|
@@ -577,12 +586,13 @@ deterministically:
|
|
|
577
586
|
|
|
578
587
|
```js
|
|
579
588
|
// 2% whales with 50x trade amount → long-tail Insights distribution
|
|
580
|
-
const isWhale = uid
|
|
589
|
+
const isWhale = hashCohort(uid, 2);
|
|
581
590
|
if (isWhale && e.event === 'swap') e.trade_amount_usd *= 50;
|
|
582
591
|
```
|
|
583
592
|
|
|
584
593
|
Use a large multiplier (≥10x, ideally 50x) so the signal beats soup noise.
|
|
585
|
-
Use
|
|
594
|
+
Use `hashCohort(uid, 2)`, `hashCohort(uid, 4)`, or `hashCohort(uid, 10)` for
|
|
595
|
+
those target percentages, then check realized eligible cohort sizes.
|
|
586
596
|
|
|
587
597
|
### Hook ordering inside `everything`
|
|
588
598
|
|
|
@@ -604,13 +614,13 @@ counts even after reduction. Either:
|
|
|
604
614
|
```js
|
|
605
615
|
// BAD — profile.level isn't in userProps; resolves to undefined
|
|
606
616
|
if (meta.profile.level >= 50) e.gold_earned *= 3;
|
|
607
|
-
//
|
|
617
|
+
// Request a schema declaration before targeting this profile segment.
|
|
608
618
|
```
|
|
609
619
|
|
|
610
620
|
When the hook references a missing profile field, you can still get the
|
|
611
621
|
data spread you want (gold range), but the cohort can't be analytically
|
|
612
|
-
recovered.
|
|
613
|
-
cohort.
|
|
622
|
+
recovered. Request the missing userProp through `/create-dungeon`, or rewrite
|
|
623
|
+
the hook to use a hash cohort without changing schema.
|
|
614
624
|
|
|
615
625
|
## Workflow
|
|
616
626
|
|
|
@@ -648,11 +658,14 @@ cohort.
|
|
|
648
658
|
If verify-dungeon returns WEAK, NONE, or INVERSE on any pattern at full
|
|
649
659
|
fidelity, return to step 4 and refine — fix the hook or the derivation,
|
|
650
660
|
never relax a threshold to match output. Iterate until all patterns score
|
|
651
|
-
|
|
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.
|
|
652
664
|
|
|
653
665
|
## Stopping condition
|
|
654
666
|
|
|
655
|
-
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,
|
|
656
669
|
OR after three iterations without convergence — at that point, document what's
|
|
657
670
|
still off in the dungeon's overview comment and report the gap to the user.
|
|
658
671
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
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
|
+
|
|
5
46
|
## 1.8.0 — 2026-09-10
|
|
6
47
|
|
|
7
48
|
### Added — `standaloneEvents`: identity-less metric snapshots
|
package/HOOKS.md
CHANGED
|
@@ -537,12 +537,22 @@ enter on birth, or drop next-day spill in an `everything` hook.
|
|
|
537
537
|
|
|
538
538
|
### 2.8 Funnel reentry: state machine resets after completion
|
|
539
539
|
|
|
540
|
-
Reference: `history.cpp` (`
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
540
|
+
Reference: `history.cpp` (`history_is_mutable`) and `funnel_query.cpp`
|
|
541
|
+
(shared first/last step handling). With `reentry: true`, completion absorbs
|
|
542
|
+
events through the inclusive 2-second grace period. The next event beyond
|
|
543
|
+
grace starts the next scan. Conversion-window expiry can restart earlier.
|
|
544
|
+
`graceperiod: false` disables the completion wait.
|
|
545
|
+
|
|
546
|
+
When an event records the ordered last step and also matches the ordered
|
|
547
|
+
first step, it closes one attempt and anchors the next immediately. Both
|
|
548
|
+
selectors must match. Any-order edges do not use this exception.
|
|
549
|
+
`woRepeat` still restarts only at window expiry.
|
|
550
|
+
|
|
551
|
+
`result.completions` reports repeat completions in uniques mode. In
|
|
552
|
+
`countMode: 'totals'`, the engine returns one `FunnelResult` per attempt,
|
|
553
|
+
including partial attempts. **Compatibility unchanged:** `reentry` defaults
|
|
554
|
+
to `false`, even for totals. Totals alone does not enable analytics general
|
|
555
|
+
counting's repeat-history behavior.
|
|
546
556
|
|
|
547
557
|
### 2.9 HPC (Hold Property Constant) — parallel sub-funnels
|
|
548
558
|
|
|
@@ -555,13 +565,24 @@ directly, or (v1.6) pass `holdPropertyConstant: '<prop>'` to the
|
|
|
555
565
|
`funnelFrequency` emulator — it routes through the HPC engine and reports
|
|
556
566
|
per-held-value sub-funnel counts.
|
|
557
567
|
|
|
568
|
+
Session windows derive ordinals from the full user stream before HPC
|
|
569
|
+
partitioning. Events with another held value can bridge a session but cannot
|
|
570
|
+
fill steps in the current bucket. This applies to explicit session windows
|
|
571
|
+
and `countMode: 'sessions'`. The local session defaults remain a 30-minute
|
|
572
|
+
timeout, 24-hour maximum, and UTC day boundaries.
|
|
573
|
+
|
|
558
574
|
### 2.10 Funnel segment modes (FIRST_TOUCH / LAST_TOUCH / STEP)
|
|
559
575
|
|
|
560
576
|
Reference: `options.hpp` `funnel_segment_mode`; `history.cpp`
|
|
561
577
|
`property_set_buffer`. The engine snapshots the matched event's properties
|
|
562
|
-
at every
|
|
563
|
-
|
|
564
|
-
|
|
578
|
+
at every reached position. FIRST_TOUCH and LAST_TOUCH merge those snapshots
|
|
579
|
+
in recorded path order, including partial and any-order paths. The first
|
|
580
|
+
or last defined non-null value wins, respectively. Undefined never replaces
|
|
581
|
+
a defined value; null never replaces a defined non-null value. If only null
|
|
582
|
+
and undefined are present, null wins. Snapshots remain unchanged.
|
|
583
|
+
|
|
584
|
+
STEP N selects one reached position without merging fallback values.
|
|
585
|
+
Enable with `evaluateFunnel(events, steps, { trackStepProperties: true })`, then pick with
|
|
565
586
|
`resolveFunnelSegment(result, 'first' | 'last' | { step: N })`.
|
|
566
587
|
|
|
567
588
|
### 2.11 Engine-validation guarantees (v1.5+)
|
|
@@ -1843,8 +1864,9 @@ event set rather than one value moment.
|
|
|
1843
1864
|
**Hook:** `everything`
|
|
1844
1865
|
**Mixpanel report:** Flows — top paths after the anchor event show the engineered branch (Section 2.17)
|
|
1845
1866
|
|
|
1846
|
-
**In Mixpanel:**
|
|
1847
|
-
|
|
1867
|
+
**In Mixpanel:** Bias the first branch toward `add to cart → begin checkout`.
|
|
1868
|
+
The helper selects ~30% of users for append-only injection. Existing traffic
|
|
1869
|
+
can interrupt the branch; the final branch share is not guaranteed to be 30%.
|
|
1848
1870
|
|
|
1849
1871
|
```js
|
|
1850
1872
|
import { applyPathBias } from "@ak--47/dungeon-master/hook-helpers";
|
|
@@ -1889,6 +1911,8 @@ if (type === "everything") {
|
|
|
1889
1911
|
sessionsPerWeek: 3,
|
|
1890
1912
|
eventsPerSession: 5,
|
|
1891
1913
|
sessionMinutes: 25,
|
|
1914
|
+
datasetStart: meta.datasetStart,
|
|
1915
|
+
datasetEnd: meta.datasetEnd,
|
|
1892
1916
|
});
|
|
1893
1917
|
}
|
|
1894
1918
|
```
|
|
@@ -1897,12 +1921,35 @@ if (type === "everything") {
|
|
|
1897
1921
|
event set (after the `everything` hook), so wholesale timestamp rewrites no
|
|
1898
1922
|
longer leave stale session labels. The atom keeps intra-session gaps well
|
|
1899
1923
|
under the 30-min timeout (spacing capped at 20min + bounded jitter), keeps
|
|
1900
|
-
inter-session gaps
|
|
1924
|
+
inter-session gaps over it when explicit bounds are supplied, and never crosses UTC midnight inside one
|
|
1901
1925
|
engineered session (the day-boundary split would cut it). Retiming only — no
|
|
1902
1926
|
events are added or dropped, so total counts and event mixes are untouched.
|
|
1903
1927
|
Session count follows `min(sessionsPerWeek × weeks, ceil(N /
|
|
1904
1928
|
eventsPerSession))`: scarce users get fewer sessions, not fabricated events.
|
|
1905
1929
|
|
|
1930
|
+
`datasetStart` and `datasetEnd` are additive, optional arguments. Existing
|
|
1931
|
+
calls with neither bound keep the original full-UTC-day placement between
|
|
1932
|
+
the user's first and last active days. A two-event stream at 12:00/12:20 can
|
|
1933
|
+
still request two sessions with `eventsPerSession: 1` and `sessionMinutes: 5`.
|
|
1934
|
+
Legacy overfull requests also keep their old behavior: they do not throw,
|
|
1935
|
+
but their clusters can merge under the 30-minute timeout.
|
|
1936
|
+
|
|
1937
|
+
The bounds accept ISO strings, unix seconds, or unix milliseconds. Hook
|
|
1938
|
+
metadata uses unix seconds and can be passed directly, as in the example.
|
|
1939
|
+
Either bound enables constrained placement; an omitted side uses the start
|
|
1940
|
+
of the first active UTC day or the end of the last active UTC day. The helper
|
|
1941
|
+
cannot infer `datasetEnd` from the last event. Pass known bounds when the
|
|
1942
|
+
dataset ends partway through an active day, including an inclusive midnight
|
|
1943
|
+
endpoint, to prevent later engine clipping.
|
|
1944
|
+
|
|
1945
|
+
With explicit bounds, partial days compress clusters, including zero-duration
|
|
1946
|
+
clusters at midnight. If the requested sessions cannot fit inside a week's
|
|
1947
|
+
available day slices, the helper throws `RangeError` before changing any
|
|
1948
|
+
events. Invalid bounds also throw. It never silently reduces the target or
|
|
1949
|
+
drops records. Widen the allowed window or reduce the session target. Exact
|
|
1950
|
+
session separation assumes UTC and the default 30-minute timeout, without
|
|
1951
|
+
a maximum session duration.
|
|
1952
|
+
|
|
1906
1953
|
---
|
|
1907
1954
|
|
|
1908
1955
|
## 5. Phase 3 Atom Reference
|
|
@@ -1933,7 +1980,7 @@ Import from `@ak--47/dungeon-master/hook-helpers`:
|
|
|
1933
1980
|
| `splitByAuth` | identity | `(events, authTime) -> { preAuth, postAuth, stitch }` | Partition by auth boundary |
|
|
1934
1981
|
| **`applyLifecycleWave`** | shape | `(events, uid, { dormantFromDay, dormantDays, resurrectBurst?, valueMomentEvent, dropAll? }) -> events[]` | Clean dormancy gap + resurrection burst; sweeps the ENTIRE window by timestamp (v1.6, recipe 4.29). Returns a NEW array |
|
|
1935
1982
|
| **`applyPathBias`** | shape | `(events, uid, { anchor, path, share, gapSeconds? }) -> events[]` | Inject a Flows path after the user's first anchor for ~`share` (fraction) of users; skips users missing any step template (v1.6, recipe 4.30) |
|
|
1936
|
-
| **`applySessionShape`** | shape | `(events, uid, { sessionsPerWeek, eventsPerSession, sessionMinutes }) -> events[]` |
|
|
1983
|
+
| **`applySessionShape`** | shape | `(events, uid, { sessionsPerWeek, eventsPerSession, sessionMinutes, datasetStart?, datasetEnd? }) -> events[]` | Preserve records; default legacy full-UTC-day placement. Optional bounds constrain placement and throw atomically on insufficient capacity (recipe 4.31) |
|
|
1937
1984
|
|
|
1938
1985
|
**Inject atoms + v1.5:** the engine auto-sorts events by time after the
|
|
1939
1986
|
`everything` hook (`autoSortAfterEverything: true` default — see Principle
|
package/README.md
CHANGED
|
@@ -957,7 +957,7 @@ styles: `support`, `review`, `search`, `feedback`, `chat`, `email`, `forum`, `co
|
|
|
957
957
|
## scripts
|
|
958
958
|
|
|
959
959
|
```bash
|
|
960
|
-
npm test #
|
|
960
|
+
npm test # default unit/integration/e2e suite; prunes data/tmp
|
|
961
961
|
npm run typecheck # typescript check
|
|
962
962
|
npm run dungeon:run # run a dungeon file locally
|
|
963
963
|
npm run dungeon:to-json # convert JS dungeon to JSON (for UI import)
|
|
@@ -978,6 +978,8 @@ node scripts/verify-runner.mjs <path> [prefix] # generate at full fidelity for
|
|
|
978
978
|
|
|
979
979
|
## tests
|
|
980
980
|
|
|
981
|
+
1.8.1 compatibility and output changes: [upgrade guide](docs/guides/1.8.1-upgrade-guide.md).
|
|
982
|
+
|
|
981
983
|
vitest tests live under `tests/` in three tiers:
|
|
982
984
|
|
|
983
985
|
| dir | scope | wall time |
|
|
@@ -999,6 +1001,40 @@ npx vitest tests/unit # watch mode
|
|
|
999
1001
|
|
|
1000
1002
|
`tests/e2e/engine-shape-full-sweep.test.js` skips itself unless `RUN_FULL_SWEEP=1` is set (it wraps the long-running 194-combo engine sweep).
|
|
1001
1003
|
|
|
1004
|
+
### editor and offline alignment tests
|
|
1005
|
+
|
|
1006
|
+
VS Code discovers unit, integration, E2E, and alignment tests through one serial
|
|
1007
|
+
`vitest.editor.config.js`. The workspace disables Go test
|
|
1008
|
+
discovery and ignores the overlapping diagnostic Vitest configs. After changing
|
|
1009
|
+
these settings, run **Developer: Reload Window** if stale providers or test runs
|
|
1010
|
+
remain in the Testing panel. Editor runs omit the pruning setup, but they are not
|
|
1011
|
+
OS-sandboxed.
|
|
1012
|
+
|
|
1013
|
+
Use **Tasks: Run Test Task** for `test: regression (offline, macOS)`, or choose
|
|
1014
|
+
the named alignment and sweep tasks from **Tasks: Run Task**. These test tasks
|
|
1015
|
+
never invoke the prune or dungeon-run tasks. Existing dungeon-run cleanup is
|
|
1016
|
+
unchanged and remains separate from testing.
|
|
1017
|
+
|
|
1018
|
+
The `test: engine matrix`, `test: engine short sweep`, and `test: engine full sweep`
|
|
1019
|
+
tasks expose the direct-run engine checks. They are opt-in and use OS network denial.
|
|
1020
|
+
The full engine wrapper is visible under E2E but skipped until `RUN_FULL_SWEEP=1`.
|
|
1021
|
+
Direct-run `.mjs` scripts are not Vitest tests, so they do not get a separate
|
|
1022
|
+
`engine` folder in the Testing tree. The legacy engine sweeps do not use the
|
|
1023
|
+
alignment runner's ten-minute deadline. E2E tests may write files or perform
|
|
1024
|
+
network operations when run directly in the editor; editor execution is not an
|
|
1025
|
+
offline guarantee.
|
|
1026
|
+
|
|
1027
|
+
```sh
|
|
1028
|
+
node tests/alignment/run.mjs # offline alignment gate
|
|
1029
|
+
node tests/alignment/run.mjs --sweep --timeout-ms=600000 # opt-in bounded sweep
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Alignment is a separate test family, excluded from `npm test` but visible in the editor.
|
|
1033
|
+
Its runner enforces OS network denial on macOS, fails closed elsewhere, and kills
|
|
1034
|
+
workers at the ten-minute deadline. Tests and reports live in the source checkout;
|
|
1035
|
+
they are not included in the npm package. The default `npm test` and direct root
|
|
1036
|
+
Vitest commands still prune `data` and `tmp` through their global setup.
|
|
1037
|
+
|
|
1002
1038
|
### engine tests (direct-run, NOT vitest)
|
|
1003
1039
|
|
|
1004
1040
|
`tests/engine/` houses direct-run regression tests at scale. these are NOT vitest-compatible — invoke with `node` directly. used to catch engine regressions across a wide variety of dungeon configurations and for ad-hoc chart inspection. outputs land in `./tmp/` (gitignored).
|