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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +9 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +35 -34
  3. package/.claude/skills/create-project/SKILL.md +6 -0
  4. package/.claude/skills/headless-build/SKILL.md +21 -11
  5. package/.claude/skills/powertools/SKILL.md +6 -2
  6. package/.claude/skills/release-check/SKILL.md +27 -2
  7. package/.claude/skills/verify-dungeon/SKILL.md +32 -13
  8. package/.claude/skills/verify-dungeon/references/alignment-contract.md +110 -0
  9. package/.claude/skills/verify-dungeon/references/counting-semantics.md +29 -16
  10. package/.claude/skills/verify-dungeon/references/report-format.md +23 -9
  11. package/.claude/skills/verify-dungeon/references/sql-recipes.md +135 -225
  12. package/.claude/skills/warehouse-metrics/SKILL.md +6 -0
  13. package/.claude/skills/write-hooks/SKILL.md +61 -48
  14. package/CHANGELOG.md +82 -0
  15. package/HOOKS.md +105 -47
  16. package/README.md +41 -1
  17. package/docs/guides/1.8.1-upgrade-guide.md +153 -0
  18. package/docs/guides/1.8.2-upgrade-guide.md +110 -0
  19. package/lib/generators/events.js +6 -0
  20. package/lib/generators/funnels.js +16 -0
  21. package/lib/hook-helpers/shape.js +73 -17
  22. package/lib/hook-patterns/attributed-by-source.js +4 -3
  23. package/lib/hook-patterns/funnel-frequency-breakdown.js +4 -7
  24. package/lib/orchestrators/user-loop.js +82 -15
  25. package/lib/verify/counting.js +7 -10
  26. package/lib/verify/emulate-breakdown.js +48 -29
  27. package/lib/verify/funnel-engine.js +93 -40
  28. package/lib/verify/identity.js +32 -9
  29. package/lib/verify/story-runner.js +93 -30
  30. package/lib/verify/verify-dungeon.js +4 -1
  31. package/package.json +1 -1
  32. package/scripts/verify-stories.mjs +3 -3
  33. package/types.d.ts +9 -5
@@ -28,8 +28,8 @@ In scope:
28
28
 
29
29
  Out of scope:
30
30
  - Schema changes (events, properties, funnels, superProps, userProps).
31
- Only modify schema if the hook can't possibly work without a new field —
32
- and even then, prefer changing the value enumeration over adding a new field.
31
+ Send missing fields or enumeration changes back to `/create-dungeon` for an
32
+ explicitly authorized schema edit before continuing. This skill never changes schema.
33
33
  - New top-level config knobs.
34
34
  - Removing the `hook: function...` body to start over with a new schema.
35
35
 
@@ -39,14 +39,15 @@ off, mix shift drags the blended rate) are better architected as initial
39
39
  conditions — duplicate funnels with swapped steps/props/`conversionRate`/
40
40
  `timeToConvert`/`weight` (see the "Structural trend engineering" section in
41
41
  `create-dungeon`). If a story reduces to structure, recommend the funnel
42
- change back to the schema instead of writing a hook to fight the engine
43
- the knob IS the expected value, which makes the story band knob-derivable
44
- (NAILED-capable) instead of confounded (STRONG-capped). Hooks are for
42
+ change back to the schema instead of writing a hook to fight the engine.
43
+ Derive the expected effect from the knob, then verify the report against a neutral
44
+ control; finite budgets and competing histories can change the measured effect. Hooks are for
45
45
  within-cohort behavior: segments doing more/less of something over time,
46
46
  property values that differ by cohort, injected bursts, lifecycle waves.
47
47
 
48
48
  ## Reference reading
49
49
 
50
+ - [1.8.1 verification contract](../verify-dungeon/references/alignment-contract.md) - independent reports, neutral controls, counting boundaries, and evidence sufficiency.
50
51
  - `lib/hook-helpers/index.js` — atoms (cohort, mutate, timing, inject,
51
52
  identity). One file per group; full JSDoc on each atom.
52
53
  - `lib/hook-patterns/index.js` — high-level recipes (one per Mixpanel
@@ -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
- **`funnel-pre` is now reliable for temporal patterns.** Usage funnels advance a
75
- cursor after each run, so successive `meta.firstEventTime` values spread across
76
- the user's active window. Persona and world-event modifiers apply BEFORE the
77
- hook the hook has final authority on `conversionRate`, `timeToConvert`, and
78
- `props`.
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 whole stream into `sessionsPerWeek` clusters of `sessionMinutes`. When-to-use: session-duration/cadence stories (sessionMetrics reads). Retiming ONLY no adds/drops; intra-session gaps stay <28min, inter-session >30min, no cluster crosses UTC midnight. Combine with `hashCohort` for per-role shapes; call BEFORE `applyPathBias` so injected paths keep their own tight gaps |
190
+ | shape | `applySessionShape(events, uid, opts)` | Retime the same records for session-duration/cadence stories. Pass known dataset bounds; explicit-bound mode rejects insufficient capacity before mutation. Legacy unbounded overfull layouts can merge sessions or cross dataset end. Call BEFORE `applyPathBias` so injected paths keep their own tight gaps. |
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' })`. Trust pre-stamped
240
- `session_id`. Engineer via `avgEventsPerUserPerDay` + `engagementDecay`.
240
+ with `emulateBreakdown({ type: 'sessionMetrics' })`. Derive sessions from
241
+ timestamps; stamped `session_id` is diagnostic or explicit legacy mode.
242
+ Engineer with `applySessionShape`, passing known dataset bounds.
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`), ALWAYS pass `profiles` to
258
- verification. Auto-builds identity map merging pre-auth `device_id`
259
- events with post-auth `user_id`.
259
+ (or the deprecated `hasAnonIds: true`), pass `profiles` for profile segments.
260
+ For identity proof, pass an explicit `identityMap` derived from emitted valid
261
+ both-ID events. Profile pools alone establish no link; the compatibility helper
262
+ can merge users without emitted stitch evidence. See the shared contract.
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 completion by per-user count(X) | funnel-post | When funnels share a step prefix, restrict scaling to the target funnel scaling every instance lets first-occurrence funnel evaluation assemble chains across unscaled instances and the ratio never reaches the report |
276
+ | `applyFunnelFrequencyBreakdown` | Funnels - completion by per-user count(X) | funnel-post | Restrict scaling to the target funnel. Completed histories can combine competing instances; verify explicit report options and paired lift instead of assuming the per-run factor equals the report ratio. |
274
277
  | `applyAggregateByBin` | Insights — avg(prop X) by per-user count(B) | everything | Same `binBy: 'distinctDays'` default as above |
275
- | `applyTTCBySegmentV2` | Funnel TTC — broken down by user-property segment | everything | v1 (`applyTTCBySegment`, funnel-post) is **deprecated**: Mixpanel TTC reads each step's FIRST occurrence per user, so per-run gap scaling only reaches the report on `isFirstFunnel` runs. V2 finds the greedy first sequence (`findFirstSequence`) and scales that |
278
+ | `applyTTCBySegmentV2` | Funnel TTC by user-property segment | everything | v1 (`applyTTCBySegment`, funnel-post) is deprecated. V2 scales `findFirstSequence`; verify the report's completed histories separately, including restart, grace, windows, and reentry. Neither helper guarantees the requested report ratio. |
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. Mixpanel derives sessions with a 30-min idle timeout and splits at
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
- atom guarantees all three; hand-rolled retiming must too.
312
+ bounded helper checks placement capacity before mutation. Legacy unbounded
313
+ overfull requests can merge sessions. Pass known bounds and measure derived
314
+ sessions; do not infer dataset end from the last observed event.
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 genuinely needs a new property and the schema doesn't have it, add
343
- the property to the EVENT CONFIG with a default value (typically `[null]` or
344
- `[false]`), not via the hook.
349
+ If a trend needs an undeclared property, stop that pattern and hand the schema
350
+ request to `/create-dungeon`. Resume only after an authorized schema edit declares
351
+ the default. Do not add the field or change its enumeration in this skill.
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 cohort math (share × numUsers × ~0.5 safety) so
467
- reduced-scale runs cap at WEAK instead of passing on noise.
473
+ - Set `minCohort` from the planned eligible population, then inspect the actual
474
+ selected-row population fields and independent users/converters. The guard
475
+ does not prove every denominator. Report insufficient evidence separately.
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. **SuperProp stamping** stamp profile values onto events (always first)
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** e.g., spring price boost, gas spike, outage errors (always LAST before sort)
482
- 6. **Sort** `userEvents.sort((a, b) => new Date(a.time) - new Date(b.time))`
489
+ 5. **Temporal value mutations that NEED cloned events** - e.g., spring price boost, gas spike, outage errors (last mutation)
490
+ 6. **Return** - the engine auto-sorts after `everything` by default; no manual output sort
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. Replace them:
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` | Only 2% of users | Increase modulus denominator or use `% 10` for 10% |
510
- | `profile.tier === "premium"` | Fixed by config distribution | Adjust userProps distribution if cohort too small |
518
+ | `charCodeAt(0) % 50 === 0` | First-character distributions are biased; modulo does not imply 2% | Use `hashCohort(uid, 2)` and measure the realized population |
519
+ | `profile.tier === "premium"` | Fixed by config distribution | Request a schema distribution change if cohort is too small |
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
- ### isStrictEvent: false is NOT optional for hook-read events
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. Your cohort goes empty.
562
+ won't emit standalone occurrences. Funnel-generated occurrences remain readable;
563
+ only a cohort that depends on additional standalone traffic needs the opt-out.
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: any event referenced in the `everything` hook by name AND appearing
563
- as a funnel step needs `isStrictEvent: false`.
572
+ Audit whether the cohort needs standalone occurrences. Request any required
573
+ `isStrictEvent: false` schema edit through `/create-dungeon`; do not change it here.
564
574
 
565
575
  ### Reentry on per-instance loops
566
576
 
567
- Funnels named "X loop" / "X cycle" / "session" / repeated user behaviors
568
- need `Funnel.reentry: true`. Without it, the engine produces ONE funnel
569
- sequence per user no recurring loops. Examples that need it: workout
570
- loop, match flow, search-to-book, order fulfillment, engagement loop, tour
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.charCodeAt(0) % 50 === 0;
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 `% 50` for ~2% whales, `% 25` for ~4% bots, `% 10` for ~10% cohorts.
594
+ Use `hashCohort(uid, 2)`, `hashCohort(uid, 4)`, or `hashCohort(uid, 10)` for
595
+ those target percentages, then check realized eligible cohort sizes.
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
- // GOOD verify by SPREAD instead, OR add level to userProps with weighted distribution
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. Either add the userProp or rewrite the hook to use a hash
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
- STRONG or NAILED.
661
+ STRONG or NAILED with report semantics, neutral controls, and sufficient
662
+ independent populations verified. Insufficient evidence requires more evidence,
663
+ not an automatic hook rewrite or a weaker target.
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,88 @@
2
2
 
3
3
  All notable changes to `@ak--47/dungeon-master`.
4
4
 
5
+ ## 1.8.2 - 2026-09-13
6
+
7
+ ### Fixed
8
+
9
+ - Build automatic verifier identity maps from emitted both-ID events. Profile
10
+ device pools no longer create links by themselves. Explicit identity maps
11
+ retain their existing behavior; conflicting authenticated events retain their user.
12
+ - Pass supported held-property session funnels through the report wrapper. Keep
13
+ implicit one-session windows when applying matching dungeon funnel defaults.
14
+ - Expand list-valued held properties into separate funnel histories, including
15
+ repeated cursor visits and full-stream session boundaries.
16
+ - Preserve prior attribution touches across time buckets, ignore unstamped
17
+ touches, and retain conversions with no matching touch in the unknown segment.
18
+ - Make `minCohort` use conservative independent-user evidence. Repeated period
19
+ rows cannot inflate populations, and custom assertions receive the same guard.
20
+ - Include declared global properties on synthetic experiment exposure events,
21
+ so run and segment filters retain both exposures and outcomes.
22
+ - Make funnel-frequency drop decisions independent of random insertion IDs.
23
+
24
+ ### Verified
25
+
26
+ - Add opt-in live comparisons through the actual importer and Mixpanel query
27
+ layer, with run-isolated identities, import budgets, retained query payloads,
28
+ exact local/live comparisons, fixed seeds, and neutral controls.
29
+ - Verify selected generated conversion, TTC, retention, volume, weight, incident,
30
+ frequency, aggregate, attribution, session, and Flows effects against live reports.
31
+ - Separate ingestion readiness from import acceptance. Historical identity links
32
+ can become visible later; one retained fixture converged without another import.
33
+
34
+ ### Compatibility
35
+
36
+ Public exports, signatures, options, and defaults remain unchanged. Corrected
37
+ counts and seeded hook outcomes can differ from 1.8.1. Explicit profile-map
38
+ overrides and totals' `reentry: false` default remain supported. Calendar-day
39
+ activity, rolling frequency, and raw event-count histograms are separate reports.
40
+ Finite-lookback attribution, ambiguous tied touches, non-UTC project settings,
41
+ and arbitrary combinations remain subject to the documented limits.
42
+
43
+ See the [upgrade guide](docs/guides/1.8.2-upgrade-guide.md) and the repository's
44
+ live alignment report for measured scope. This entry does not indicate npm publication.
45
+
46
+ ## 1.8.1 — 2026-09-12
47
+
48
+ ### Fixed
49
+
50
+ - Align funnel counting with source-derived Mixpanel contracts: shared first/last
51
+ steps finalize correctly with or without reentry, completion grace consumes
52
+ events through the inclusive two-second boundary, and first/last-touch
53
+ breakdowns merge properties across reached steps.
54
+ - Derive session boundaries from the full event stream before hold-property-constant
55
+ partitioning, so unrelated activity can keep a session open.
56
+ - Keep onboarding after user creation and usage after onboarding. Preserve retry
57
+ entries when strict event-count capacity allows. Short windows retain partial
58
+ output and report capacity limits through existing warnings.
59
+ - Reconcile engine-generated identity against surviving auth events, including
60
+ engine-created duplicates and amplification clones. Preserve explicit hook
61
+ overrides and the synthetic experiment identity exception.
62
+ - Select the earliest chronological anchor for append-only path injection.
63
+
64
+ ### Added
65
+
66
+ - Optional `datasetStart` and `datasetEnd` bounds for `applySessionShape`, preventing
67
+ retimed events from being clipped outside a known dataset window. Explicit bounds
68
+ reject impossible session layouts before mutation; calls without bounds retain
69
+ legacy full-day placement.
70
+ - An offline `tests/alignment` family with mixed-dungeon trend checks, neutral
71
+ controls, multiple seeds, sample-size guards, counting contracts, and a diagnostic
72
+ sweep with a hard ten-minute deadline. The runner denies network access at the OS
73
+ level on macOS and fails closed elsewhere.
74
+ - A control inventory, failure-first repair evidence, and measured operating ranges.
75
+ Release validation passed 175 alignment tests and 2,022 unit/integration tests, with
76
+ one existing skip. The 297-cell sweep generated 17.08 million events cumulatively:
77
+ 125 cells met their evidence thresholds and 172 had insufficient evidence.
78
+
79
+ ### Compatibility
80
+
81
+ Existing call forms and defaults remain supported. Session bounds are additive,
82
+ not required. Corrected timestamps, identities, and counts can change generated
83
+ output relative to 1.8.0. Totals counting still requires explicit `reentry: true`
84
+ for repeated histories. These tests use local Mixpanel source-derived contracts,
85
+ not live engine execution, and do not establish complete coverage of every knob.
86
+
5
87
  ## 1.8.0 — 2026-09-10
6
88
 
7
89
  ### Added — `standaloneEvents`: identity-less metric snapshots