@ak--47/dungeon-master 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
package/CHANGELOG.md CHANGED
@@ -2,6 +2,188 @@
2
2
 
3
3
  All notable changes to `@ak--47/dungeon-master`.
4
4
 
5
+ ## 1.5.1 — 2026-05-20
6
+
7
+ Quality + ergonomics release. No new analytical capabilities — fixes accumulated rough edges around concurrency, accuracy, profiles, and config ergonomics that surfaced after 1.5.0 shipped. Adds a generator-side retention shaper, exposes a config sub-object API for cleaner dungeon files, and restructures all 48 shipped dungeons to a canonical layout. Top-level keys keep working for back-compat.
8
+
9
+ ### Added
10
+
11
+ - **`credentials` / `switches` / `identity` config sub-objects.** New ergonomic shape for grouping related dungeon keys: `credentials: { token, region, serviceAccount, serviceSecret, projectId }`, `switches: { hasLocation, hasCampaigns, hasSessionIds, hasAvatar, isAnonymous, ... }`, `identity: { avgDevicePerUser, sessionTimeout }`. `mergeConfigSubObjects` hoists sub-object values into top-level keys at validation time; top-level still wins when both set (with a verbose warn). Old flat top-level keys keep working — back-compat suite in `tests/unit/config-restructure.test.js`.
12
+ - **`retentionCurve` config knob.** Generator-side retention shaper. Accepts an array of `{day, retention}` waypoints; the engine interpolates log-linearly to drop late events per user based on first-event age. Independent of `engagementDecay`. Enables analytical-style retention shapes (D1 80% → D7 50% → D30 20%) at dungeon-config level instead of via hooks.
13
+ - **Per-macro `avgActiveDaysPerUser` defaults.** When `avgActiveDaysPerUser` is unset, defaults derived per macro: steady=15, growth=10, viral=20, decline=5, flat=20 (numDays/4 cap). Removes the need to hand-tune for every macro.
14
+ - **`COUNT_DISTINCT` aggregation in `emulateBreakdown`** (`type: 'distinctCount'`). Mirrors Mixpanel's count-distinct measure for cohort sizing / unique-user breakdowns.
15
+ - **`userSeed` config knob.** Separate distinct_id seed from the main `seed`. Lets you regenerate a dataset with a different event distribution while keeping the user pool stable across runs — useful for incremental data layering.
16
+ - **`result.profilesPushed`** count exposed on the `Result` object. Reports how many profiles actually got pushed to `/engage` after `_drop` filtering.
17
+ - **`runWithDataset(begin, now, fn)`** API for explicit dataset-window scoping (rare — most callers don't need this; in-process `generate()` calls now auto-scope via AsyncLocalStorage).
18
+
19
+ ### Changed
20
+
21
+ - **Anonymous non-converters get `_drop: true` stamped on their profile.** Real-world Mixpanel `$identify` semantics — profiles only exist for users who actually identified. Born-in-dataset users who never reach an `isAuthEvent` step in their first funnel are anonymous: events still flow (tied to `device_id`), but no profile is pushed to `/engage`. `userProfilesData` still contains every profile object; `mixpanel-sender` filters `_drop:true` before push. Pre-existing users are considered already-identified and never get `_drop`. The `everything` hook can rescue a profile by `delete meta.profile._drop`.
22
+ - **`numEvents` more accurate.** Removed dice rolls + `0.714` magic dampening from per-user budget computation; replaced with a clean `chance.normal(mean=budget, dev=budget/3)`. Old behavior overshot the target by 1.6-2x; new behavior matches the configured rate within ±3% across all 5 macros at 50K target. **If you previously tuned `avgEventsPerUserPerDay` around the old overshoot, expect ~40-60% fewer events at the same rate.** Recompute targets.
23
+ - **Default `Platform` device property removed.** 59 entries commented out in `lib/templates/defaults.js` (15 iOS + 15 Android + 29 Desktop). `os` already carries the platform signal. If a dungeon reads `Platform` in hooks or downstream, you'll see undefined — define `Platform` explicitly in your event properties to opt back in.
24
+ - **`hasAnonIds` deprecated.** Use `identity.avgDevicePerUser: 1` instead. The deprecated alias still works through 1.5.x: when `hasAnonIds: true` is set without an explicit `avgDevicePerUser`, the validator promotes to `avgDevicePerUser: 1`.
25
+ - **`DATASET_NOW` / `DATASET_BEGIN` scoped via AsyncLocalStorage.** No more module-level mutable globals. In-process concurrent `generate()` calls with different `datasetStart`/`datasetEnd` windows now produce isolated, in-window output. Legacy `setDatasetNow` / `setDatasetBegin` setters remain as back-compat shims.
26
+ - **UTC bare-date parsing** for `datasetStart` / `datasetEnd`. `"2026-01-01"` parses as `2026-01-01T00:00:00Z`, not as local-midnight (which shifted the window by UTC offset).
27
+ - **Quiet by default.** `verbose: false` (default) now gates all warnings, info logs, and dataset-context messages. Set `verbose: true` to opt back into the chatty output.
28
+ - **GCS upload retry hardening.** 10 retries with exponential backoff, 10-minute total budget. Handles transient cloud upload failures without giving up.
29
+ - **>64K user runs no longer crash V8.** Internal data structure swap (`Array` for sparse-keyed maps) eliminates V8 limit hit at high user counts.
30
+ - **All 48 shipped dungeons restructured to canonical layout.** Sections in fixed order: IMPORTS → OVERVIEW → HOOK STORIES → SCALE → KNOBS → DATA ARRAYS → HOOK STATE → HELPER FUNCTIONS → CONFIG. Hook stories preserve full per-hook Mixpanel report docs; `config.hook` becomes a thin dispatcher delegating to per-type helpers (`handleEventHooks`, `handleEverythingHooks`, etc.). Zero behavioral changes — every dungeon's seed-pinned output is unchanged.
31
+ - **All 49 dungeons + ~18 test fixtures migrated to the new sub-object API.** Cosmetic adoption only — `mergeConfigSubObjects` already supported both shapes since Phase 1.
32
+
33
+ ### Fixed
34
+
35
+ - **Standalone events now stamp `config.superProps`.** Was silently `{}` before — masked by the validator's auto-funnel pre-fix in 1.5.0. Surfaced by the `numEvents` overshoot fix when the `useFunnel` gate started routing more users to the standalone path.
36
+ - **Born-late funnel auth events past `FIXED_NOW` no longer set `userAuthTimeMs`.** Engine drops the event at storage time (`funnels.js:640`) but previously still recorded the auth-time, marking the user as authed without a real sign_up event. Fix gates `authTimeMs` on `!_drop` (`funnels.js:328`). Affected 1-2% of users in test runs.
37
+ - **Pre-existing user events strict-clamp at `FIXED_BEGIN`.** Born-outside-window users no longer leak events into the pre-dataset window via TimeSoup's sub-window distribution.
38
+
39
+ ### Docs
40
+
41
+ - **HOOKS.md targeted edits.** Recipe 4.25 (First-Touch Attribution Bias) gains a v1.5 note pointing to Recipe 4.26's OVERWRITE pattern when `hasCampaigns: true`. Atom catalog gains a footnote that `injectBetween` / `injectBurst` / `injectAfterEvent` / `injectOnNewDays` no longer require trailing `record.sort(...)` calls — covered by `autoSortAfterEverything: true` default since 1.5.0.
42
+ - **`docs/guides/1.5.1-upgrade-guide.md`** — TL;DR + per-change action items for existing dungeon authors.
43
+
44
+ ### Infra
45
+
46
+ - 95+ commits across the branch; 1269 vitest tests pass; engine canary 10/10; engine-shape full sweep 194/194; smoke test 20/20 verticals; 5-vertical hook verifier matches Sprint 1 baseline (ecommerce 10/10, fitness 12/12, sass 10/10, social 11/11, dating 11/13 pre-existing small-mode artifacts).
47
+
48
+ ## 1.5.0 — 2026-05-08
49
+
50
+ The "count and verify like Mixpanel does" release. Aligns BOTH the data generation engine AND the verifier with Mixpanel's actual counting semantics — greedy single-pass funnels, distinct-period frequency counting, null-aware aggregation, touchpoint-capped attribution, identity merge, retention, sessions, time-bucketed trends. Removes `bunchIntoSessions`, the root cause of funnel ordering corruption since 1.0.
51
+
52
+ ### Generator changes
53
+
54
+ #### Added
55
+ - **`avgActiveDaysPerUser`** — Concentrates events onto fewer distinct UTC days per user. Uses weighted-without-replacement day picking from soup DOW weights. Events per active day scale naturally (`rate × remaining_days ÷ active_days`). Interacts correctly with `engagementDecay` (protects last event per picked day from being dropped).
56
+ - **`conversionWindowDays`** on funnels — Explicit conversion window (default 30, hard cap 180). Validator auto-bumps when `timeToConvert` exceeds the default. Funnel generator caps step-to-step time to the window. Verifier and `emulateBreakdown` apply the same window.
57
+ - **`maxTouchpointsPerUser`** — Per-user touchpoint cap (default 10) matching Mixpanel's `attributed_value_reader.cpp`. Engine samples eligible events across user lifetime using `chance.pickset`, stamps UTMs on the sample only. Replaces the old inline 25% UTM stamping in `events.js`.
58
+ - **`autoSortAfterEverything`** — Auto-sorts user events by time after the `everything` hook (default `true`). Defends greedy funnel verification from out-of-order hook-injected events. Opt out with `autoSortAfterEverything: false`.
59
+ - **`isStrictEvent` auto-promote** — Config validator detects events that appear in both `events[]` and `funnels[].sequence` and auto-promotes them to `isStrictEvent: true`. Prevents greedy engine corruption where standalone instances of funnel-step events confound conversion counting. Opt out per-event with `isStrictEvent: false`. Runs BEFORE catch-all funnel creation.
60
+ - **`Funnel.exclusionEvents: string[]`** — events that terminate the funnel for non-converters. Generator stamps 1-2 cloned events bearing one of the listed names between the last completed step and where the next step would have been. Schema-first: validator throws on undeclared entries; cloned events copy ONLY identity + super props + group keys + props declared on the exclusion event's own config (no source-event prop pollution).
61
+ - **`Funnel.reentry: boolean`** — verifier-only hint. Auto-applied by `verifyDungeon` to matching `funnelFrequency` / `timeToConvert` checks.
62
+ - **`Funnel.stepFilters: Record<number, { prop, op, value }>`** — verifier-only hint. The verifier attaches `where`-clauses at the matching step index.
63
+ - **Session day-boundary split** — `assignSessionIds` now ends a session at the UTC day boundary (matches Mixpanel `session_query.cpp:828-830, 911`). Three reset triggers: timeout gap > 30 min, max session > 24h, OR day-index change.
64
+ - **`weightedSampleNoReplacement`** — Seeded weighted sampling utility for active-day picking and touchpoint selection.
65
+
66
+ #### Changed
67
+ - **`bunchIntoSessions` removed.** Was a wholesale timestamp overwrite that scrambled funnel ordering and destroyed TimeSoup's time distribution. Replaced by natural TimeSoup-driven timestamps + `assignSessionIds` (which was already running but had its work overwritten by `bunchIntoSessions`). Events now arrive in correct temporal order without post-hoc rewriting.
68
+ - **Standalone events use `isFirstEvent: false`.** Previously all standalone events used `isFirstEvent: true`, pinning them to the same timestamp. The old `bunchIntoSessions` retimed them — now TimeSoup distributes them directly.
69
+ - **UTM stamping moved to per-user pass.** Inline per-event UTM stamping in `events.js` replaced by `applyTouchpointCap` in `user-loop.js`. Runs after all events are generated, samples up to `maxTouchpointsPerUser` eligible events. Matches Mixpanel's attribution counting behavior.
70
+ - **Funnel generator respects conversion window.** When a funnel's step-to-step span exceeds `conversionWindowDays`, the generator scales `relativeTimeMs` to fit.
71
+ - **`funnelFeatureCtx` preserves `latestTime`.** Bug fix: was dropping `featureCtx.latestTime`, causing funnel first events to use the full `[earliest, FIXED_NOW]` range instead of the picked day's bounds in active-day mode.
72
+ - **Empty event pool bail-out.** When all events are funnel steps (auto-promoted to strict) and there are no standalone events, the user loop skips standalone generation instead of crashing on `pick([])`.
73
+ - **Future-event filter now logs in verbose mode.** Events past `FIXED_NOW` (from catch-all funnel TTC drift) are filtered with a verbose log showing count, user, and time range.
74
+ - **`buildActiveDayPlan` returns `pickedDayBuckets`.** Shape changed from `number[] | null` to `{ plan, pickedDayBuckets } | null` so engagement decay can protect last events on active days.
75
+ - **User loop wrapped in try/finally.** SIGINT cleanup (progress interval, user count reset) runs even on error.
76
+
77
+ ### Verifier changes
78
+
79
+ The "verify like Mixpanel does" half. Adds counting primitives behind `emulateBreakdown` so engineered hook patterns can be verified against the same shapes Mixpanel computes.
80
+
81
+ #### Added
82
+ - **Identity resolution** — `buildIdentityMap(profiles)` inverts each profile's `device_ids` / `anonymousIds` (legacy field name supported) into a flat `Map<device_id, canonical_user_id>`. `resolveUserId(event, identityMap)` resolves a single event with priority: `event.distinct_id` → identity map → `event.user_id` → `event.device_id` (Mixpanel canonical post-merge id wins). `emulateBreakdown` auto-builds the map when `profiles` are passed (any breakdown type, hoisted above `timeBucket` recursion to avoid rebuild per partition).
83
+ - **Funnel engine extensions** — `evaluateFunnel` accepts:
84
+ - `reentry: boolean` — re-runs state machine after each completion; `result.completions` reports total.
85
+ - `exclusionSteps: [{ event, afterStep?, beforeStep? }]` — events that terminate the current attempt. `afterStep`/`beforeStep` use the index of the step that must (have) been reached; defaults `afterStep=-Infinity`, `beforeStep=steps.length` (fires anywhere, used by simple `Funnel.exclusionEvents` shape). Cooperates with reentry.
86
+ - Step filters — steps may be `{ event, where: { prop, op, value } }`. Supported ops: eq, neq, gt, lt, gte, lte, contains, not_contains.
87
+ - `trackStepProperties: boolean | string[]` — captures matched event properties at each step into `result.stepProperties`.
88
+ - `countMode: 'uniques' | 'totals'` — totals mode returns `FunnelResult[]` (Mixpanel `funnel_query.cpp:2055-2100`). Includes incomplete attempts so per-step drop-off counts are preserved (`history_get_reached >= 0`, NOT "completed"). Without reentry: single-attempt array.
89
+ - `sessionScoped: boolean` — partition by `session_id`, run per session. Verifier-only convenience; Mixpanel's closest analog is `WINDOW_TYPE_SESSIONS` on the conversion window.
90
+ - **HPC** — `evaluateFunnelHPC(events, steps, holdProperty, options)` runs parallel sub-funnels per unique value of the held property on the step-0 event. Returns `Map<value, FunnelResult | FunnelResult[]>`. NOT auto-routed through `funnelFrequency` (different report shape); call directly inside `verifyDungeon` checks.
91
+ - **Segment modes** — `resolveFunnelSegment(result, 'first' | 'last' | { step: N })` picks property snapshot for FIRST_TOUCH / LAST_TOUCH / STEP modes.
92
+ - **`emulateBreakdown({ type: 'sessionMetrics' })`** — group by user→session, emit `[{ metric, avg, median, p90, total_sessions }]` for count / duration / eventsPerSession. Trusts pre-stamped `session_id`. Optional `event` filter restricts to sessions containing a target event (verifier-only convenience).
93
+ - **`emulateBreakdown({ type: 'retention' })`** — birth-anchored ms-delta bucketed retention (`retention_query.cpp:1227-1231`). A return 23h after birth lands in bucket 0; 25h lands in bucket 1. `birthCanRetain: false` default (`retention_query.cpp:1097-1109`). Inputs `cohortEvent`, `returnEvent`, `dayBuckets`. Optional `segmentBy` partitions cohort by birth event property (`segment_event=FIRST` mode); optional `carry_forward` marks once-retained users as retained on later buckets (CARRY_FORWARD unbounded mode).
94
+ - **`emulateBreakdown({ timeBucket: 'day' | 'week' | 'month' })`** — cross-cutting wrapper on every breakdown type. Partitions events by UTC bucket, tags rows with `period: string` (`YYYY-MM-DD`, `YYYY-Www`, `YYYY-MM`). Optional `timeBucketRange: { from, to }` enumerates every bucket and emits `{ period, _empty: true }` markers for empty intervals (Mixpanel `normal_query.cpp:352-356` parity).
95
+ - **`aggregatePerUser` cohort-level rollup** — `cohort_sum` / `cohort_min` / `cohort_max` field added when the per-user `agg` is `sum`/`count`/`min`/`max`. `avg_aggregate` always populated. Matches Mixpanel's "Aggregate per user" report column for the corresponding agg mode.
96
+ - **`partitionByTimeBucket(events, bucket, options?)`** — exposed helper. Accepts `{ from, to }` for empty-bucket enumeration.
97
+ - **`evaluateAnyOrderCompletion`** — Verifier function for `unordered`/`random` funnel modes. `emulateBreakdown` auto-dispatches based on `funnel.order`.
98
+
99
+ #### Changed
100
+ - **Identity resolver order** — `event.distinct_id` now wins over the merge map (Mixpanel canonical post-merge id; never demote).
101
+ - **Sessions split on UTC day** — `session_query.cpp:828-830` parity (added to `assignSessionIds` AND `sessionMetrics`).
102
+
103
+ ### Documented divergences (intentional v1.5.0 scope gaps)
104
+
105
+ - **HPC list-property values** — scalar only; Mixpanel `aggregate_hash_get_key_cursor` explodes list values into N sub-funnels per event.
106
+ - **`sessionScoped` funnel** + **`sessionMetrics({ event })`** — verifier-only conveniences; not directly reproducible in Mixpanel UI.
107
+ - **Retention COMPOUNDED, CARRY_BACK, CONSECUTIVE_FORWARD, CALENDAR_START, segment_event=SECOND, cohort window, week/month bucket units** — out of v1.5.0 scope.
108
+ - **Timezone** — verifier uses UTC; Mixpanel uses query timezone (qtz).
109
+ - **Percentiles** — linear interpolation (d3.quantile); Mixpanel uses TDigest.
110
+ - **Selector grammar** — eq/neq/gt/lt/gte/lte/contains/not_contains only; no is_set/between/regex/contains_ci.
111
+
112
+ ### Backward Compatibility
113
+
114
+ - **No breaking changes to the public API.** `DUNGEON_MASTER(config)` signature unchanged. All named exports unchanged.
115
+ - Existing dungeons run without modification. New config fields are additive and optional.
116
+ - `bunchIntoSessions` removal changes timestamp distribution for all dungeons. Events now follow TimeSoup's natural distribution instead of being rewritten into synthetic session clusters. This is more correct — funnels maintain temporal ordering.
117
+ - `isStrictEvent` auto-promote may reduce standalone event variety for dungeons where funnel-step events overlap with `events[]`. Add `isStrictEvent: false` on specific events to preserve standalone instances.
118
+ - Touchpoint cap (default 10) reduces UTM-stamped events from ~25% of all events to at most 10 per user. Attribution analysis produces more realistic distributions.
119
+ - UTC day-boundary session split may produce more sessions per user than 1.4 (sessions crossing midnight now split). Session metrics shift accordingly.
120
+
121
+ ### Documentation
122
+
123
+ - **`research/1.5.0-upgrade-guide.md`** — consumer upgrade guide with behavioral changes, new verifier capabilities, identity-aware verification requirement.
124
+ - **CLAUDE.md** updated with `avgActiveDaysPerUser`, `conversionWindowDays`, `maxTouchpointsPerUser`, `autoSortAfterEverything`, active-day distribution section, 15-step execution order.
125
+ - **HOOKS.md** — §2.4 touchpoint generation contract, §2.5 active-day distribution, §2.6–2.10 (sessions, retention, reentry, HPC, segment modes), §8 v1.5.0 verification recipes (8 patterns).
126
+ - **Skill files** updated: `create-dungeon`, `write-hooks`, `verify-dungeon` with v1.5 considerations + new primitive table.
127
+
128
+ ### Engine validation + strict clamps (post-eval ship gate)
129
+
130
+ Final 1.5.0 hardening pass — proves the engine produces clean, in-band charts across the param space and adds validator guards against the worst foot-guns. Methodology + sweep evidence: `plans/ENGINE-VALIDATION/FIX.md`.
131
+
132
+ #### Engine
133
+
134
+ - **`FUNNEL_DEAD_ZONE_CAP_SEC = 0`** (`lib/orchestrators/user-loop.js`). Earlier rounds reserved a 1-day "dead zone" before `FIXED_NOW` for funnel step-1 anchors to defend against a cursor-accumulation bug. Round 1 fixed the cursor accumulation directly, leaving the cap as defense-in-depth. The future-time guard at storage step 14 already drops any `time > FIXED_NOW`, so removing the cap is safe — funnels can now anchor right up to `FN`. Eliminates the last-day cliff for funnel-heavy dungeons. Verified across 194-combo sweep: `futureEvents == 0` everywhere.
135
+
136
+ #### Validator strict clamps (`lib/core/config-validator.js`)
137
+
138
+ Seven clamps with `console.warn` messages explaining what changed and why. All fire either unconditionally (sanity bounds) or only when the user explicitly overrides via top-level field OR `macro: { preset, ... }` object override (raw preset names exempt — preset values are designed to be safe).
139
+
140
+ | # | Clamp | Trigger | Action |
141
+ |---|-------|---------|--------|
142
+ | 1 | `percentUsersBornInDataset` ∈ [0, 100] | Always | Clamp + warn |
143
+ | 2 | Per-macro born cap (flat=12, steady=12, growth=30, viral=55, decline=5) | User-explicit `macro` AND user-explicit `percentUsersBornInDataset` | Clamp + warn |
144
+ | 3 | `bornRecentBias` ∈ [-0.5, 0.5] | User-explicit (incl. macro-object override) | Clamp + warn |
145
+ | 4 | Compound: `born > 60 && bias > 0.4` → bias=0.3 | User-explicit (either) | Clamp + warn |
146
+ | 5 | `bornRecentBias` ∈ [-1, 1] (`Math.pow` guard) | Always | Clamp |
147
+ | 6 | `avgEventsPerUserPerDay` > 50 → 50 | Always | Clamp + warn (recompute `numEvents`) |
148
+ | 7 | `avgActiveDaysPerUser` > `numDays * 0.5` → `floor(numDays * 0.5)` | Always | Clamp + warn |
149
+
150
+ Plus a warning-only check for `numDays < 14` (window may be pinned via `datasetStart`/`datasetEnd` upstream).
151
+
152
+ #### Sweep harness (`scripts/sweep-engine.mjs`)
153
+
154
+ Validates `dungeons/technical/simplest.js` (no-hook baseline) across a 194-combo cross-product matrix of macro × numDays × born × rate × activeDays. Per-macro strict bars match each preset's design intent (flat is stationary, viral is hockey-stick). Pinned to most-recent past Wednesday-EOD-UTC anchor for full calendar-day determinism — back-to-back runs produce zero metric drift. **194 / 194 PASS.**
155
+
156
+ `tests/unit/engine-shape-canary.test.js` — 10-test ~5s canary (runs every commit) with fixed-date pinning (`datasetEnd = '2026-04-30T23:59:59Z'`).
157
+
158
+ `tests/e2e/engine-shape-full-sweep.test.js` — gated by `RUN_FULL_SWEEP=1`, runs the full 194-combo matrix (~5.5 min). Pre-release acceptance gate.
159
+
160
+ #### Hook compatibility
161
+
162
+ Spot-checked 5 verticals post-fix: **56 / 56 hook checks PASS** (fitness 12, dating 13, ecommerce 10, sass 10, social 11). Hook magnitudes match prior eval within ±10%. Engine fix is hook-compatible at full fidelity. None of the 20 vertical dungeons set `percentUsersBornInDataset` / `bornRecentBias` / `avgEventsPerUserPerDay > 50` explicitly → validator clamps don't fire on existing dungeons.
163
+
164
+ #### Documentation
165
+
166
+ - **CLAUDE.md** — new "Tuning guidance — safe ranges and engine guarantees (v1.5)" section under "Trend Shape — Macro and Soup". Per-tunable safe-range table, 6 strict-bar conditions, per-macro bar values, known-engine-guarantees subsection.
167
+ - **types.d.ts** — JSDoc `safe range` + clamp behavior on `numDays`, `percentUsersBornInDataset`, `bornRecentBias`, `avgEventsPerUserPerDay`, `avgActiveDaysPerUser`.
168
+ - **`.claude/skills/create-dungeon/SKILL.md`** — macro × born% compatibility note + clamp warnings.
169
+ - **`.claude/skills/write-hooks/SKILL.md`** — "intentional strict-bar deviation" pattern (decline + churn cohorts and viral-with-persona-lift can intentionally exceed bars).
170
+
171
+ ### Test Suite
172
+
173
+ Full suite: **46 files, 1100+ tests** (was 960 in 1.4). Engine-validation pass adds 12 (10 canary + 2 clamp + 4 macro-object form, minus updates) → **1122 passed / 2 skipped**. Highlights:
174
+
175
+ | File | Tests | Coverage |
176
+ |------|-------|----------|
177
+ | Generator: `active-days`, `conversion-window`, `order-mode-dispatch`, `touchpoint-cap`, `strict-event-autopromote`, `auto-sort`, `interrupt-funnel`, `datagen-determinism`, `decay-respects-active-days` | 41 | Engine changes |
178
+ | `tests/unit/identity-resolution.test.js` | 14 | Map inversion, resolver fallback chain |
179
+ | `tests/unit/funnel-engine.test.js` (extended) | 51 | Reentry, exclusion, HPC, step filters, step properties, segment modes, sessionScoped — 5+ ported fixtures from `test_qt_funnel.py` |
180
+ | `tests/unit/session-metrics.test.js` | 11 | count/duration/eventsPerSession + day-boundary + ported `test_qt_sessions.py` fixture |
181
+ | `tests/unit/retention.test.js` | 9 | ms-delta bucketing + birthCanRetain + carry_forward + segmentBy + ported `test_qt_retention.py` fixture |
182
+ | `tests/unit/time-bucketed.test.js` | 11 | day/week/month + cross-cutting + empty backfill |
183
+ | `tests/integration/identity-model.test.js` (extended) | +1 | `emulateBreakdown` profile-merge round-trip |
184
+ | `tests/integration/hook-patterns-emulator.test.js` (extended) | +7 | Funnel options + new breakdown types + cohort SUM/MAX |
185
+ | `tests/integration/features.test.js` (extended) | +4 | exclusionEvents validator + injection + schema-clean clone |
186
+
5
187
  ## 1.4.5 — 2026-05-06
6
188
 
7
189
  ### Added