@ak--47/dungeon-master 1.5.3 → 1.6.0

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 (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +96 -0
  4. package/.claude/skills/create-project/provision.mjs +336 -0
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +211 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +3 -2
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. package/dungeons/vertical/travel.js +0 -930
package/types.d.ts CHANGED
@@ -147,7 +147,11 @@ export interface Dungeon {
147
147
  region?: "US" | "EU";
148
148
  /** User generation concurrency. Default: 1. Values > 1 break seed reproducibility and provide no performance benefit (CPU-bound). */
149
149
  concurrency?: number;
150
- /** Number of records before auto-flushing to disk. Prevents OOM for large datasets. Default: 1,000,000 */
150
+ /**
151
+ * Number of records before auto-flushing to disk. Prevents OOM for large datasets.
152
+ * Default: 2,500,000. Auto-enabled at 1,000,000 when the projected event count
153
+ * reaches 2M and no explicit value is set (clamped lower when avgPropsPerEvent > 18).
154
+ */
151
155
  batchSize?: number;
152
156
 
153
157
  // ── Mixpanel Import Credentials (for SCD import) ──
@@ -1029,9 +1033,10 @@ export interface Funnel {
1029
1033
  * - `ExperimentConfig` object — custom variant names, conversion/TTC multipliers, temporal gating,
1030
1034
  * and distribution weights.
1031
1035
  *
1032
- * Variant assignment is **deterministic per user** (hash of user_id + experiment name), so the same
1033
- * user is in the same variant across all funnel runs. `$experiment_started` is prepended to the
1034
- * sequence for every post-start-date funnel run.
1036
+ * Variant assignment is **sticky by default** — deterministic per user (hash of user_id +
1037
+ * experiment name), so the same user is in the same variant across all funnel runs. Set
1038
+ * `sticky: false` on the config object to re-randomize the variant on every funnel pass.
1039
+ * `$experiment_started` is prepended to the sequence for every post-start-date funnel run.
1035
1040
  *
1036
1041
  * Hook meta (`meta.experiment`) exposes the resolved variant in `funnel-pre` and `funnel-post`
1037
1042
  * hooks, enabling variant-specific story injection.
@@ -1077,7 +1082,7 @@ export interface Funnel {
1077
1082
  }>;
1078
1083
 
1079
1084
  /** @internal Resolved experiment config set by config-validator. */
1080
- _experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null };
1085
+ _experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null; sticky: boolean };
1081
1086
  /** @internal Set by funnels.js during experiment handling. */
1082
1087
  _experimentName?: string;
1083
1088
  /** @internal Set by funnels.js during experiment handling. */
@@ -1151,6 +1156,15 @@ export interface ExperimentConfig {
1151
1156
  * Default: 0 (entire dataset).
1152
1157
  */
1153
1158
  startDaysBeforeEnd?: number;
1159
+ /**
1160
+ * Sticky bucketing. When `true` (default), variant assignment is a deterministic
1161
+ * hash of user_id + experiment name — once a user falls into a variant they get
1162
+ * that variant on every funnel pass (matches Mixpanel experiment SDK behavior and
1163
+ * makes variant trends verifiable). When `false`, the variant is re-rolled on
1164
+ * every funnel pass using the seeded RNG, so a multi-pass user may see different
1165
+ * variants across passes.
1166
+ */
1167
+ sticky?: boolean;
1154
1168
  }
1155
1169
 
1156
1170
  /** A single variant in an experiment. */
@@ -1380,15 +1394,29 @@ export interface Persona {
1380
1394
  eventMultiplier?: number;
1381
1395
  /** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). */
1382
1396
  conversionModifier?: number;
1383
- /** Base churn rate for this persona (0-1). */
1397
+ /**
1398
+ * Base churn rate for this persona (0-1).
1399
+ * @deprecated — unimplemented; no-op. The validator defaults it to 0 but no
1400
+ * generator reads it. Model churn with hooks instead (HOOKS.md §3 cohort
1401
+ * atoms + `engagementDecay`).
1402
+ */
1384
1403
  churnRate?: number;
1385
1404
  /** Properties merged into user profiles for this persona. */
1386
1405
  properties?: Record<string, ValueValid>;
1387
- /** Limit how long this persona is active (e.g., trial users active for 14 days). */
1406
+ /**
1407
+ * Limit how long this persona is active (e.g., trial users active for 14 days).
1408
+ * @deprecated — unimplemented; no-op. Declared surface only; nothing in lib/
1409
+ * reads it. Bound activity windows with an `everything` hook (drop events
1410
+ * outside the window) instead.
1411
+ */
1388
1412
  activeWindow?: { maxDays: number };
1389
1413
  /** Per-persona engagement decay override. */
1390
1414
  engagementDecay?: EngagementDecay;
1391
- /** Per-persona soup/timing override. */
1415
+ /**
1416
+ * Per-persona soup/timing override.
1417
+ * @deprecated — unimplemented; no-op. Declared surface only; nothing in lib/
1418
+ * reads it. Use the top-level `soup` config for timing shape.
1419
+ */
1392
1420
  soupOverride?: SoupConfig;
1393
1421
  }
1394
1422
 
@@ -1581,6 +1609,110 @@ export declare function dungeonToJSON(input: string[], options?: { includeCreden
1581
1609
  export declare function extractComments(input: string): DungeonComments;
1582
1610
  export declare function extractComments(input: string[]): DungeonComments[];
1583
1611
 
1612
+ // ============= Story-Spec Types (v1.6) =============
1613
+
1614
+ /**
1615
+ * Closed enum of engineered-effect shapes, seeded from the corpus's real story
1616
+ * types. Mirrors `lib/templates/story-spec.schema.json` (a unit test keeps the
1617
+ * two in sync).
1618
+ */
1619
+ export type StoryArchetype =
1620
+ | "cohort-count-scale"
1621
+ | "cohort-prop-scale"
1622
+ | "temporal-inflection"
1623
+ | "funnel-conversion-by-segment"
1624
+ | "funnel-ttc-by-segment"
1625
+ | "retention-divergence"
1626
+ | "frequency-sweet-spot"
1627
+ | "attribution-bias"
1628
+ | "experiment-lift"
1629
+ | "lifecycle-wave"
1630
+ | "path-share"
1631
+ | "session-shape"
1632
+ | "composition-drift"
1633
+ | "bespoke";
1634
+
1635
+ /**
1636
+ * Verdict tiers, worst → best: INVERSE < NONE < WEAK < STRONG < NAILED.
1637
+ * NAILED = observed within ±10% of `target`; STRONG = passes `floor` (or
1638
+ * `target` when no floor); WEAK = fails floor but effect direction correct, or
1639
+ * cohort < `minCohort`; NONE = no measurable effect / selection empty;
1640
+ * INVERSE = effect direction opposite the assertion. Story verdict = worst
1641
+ * assertion.
1642
+ */
1643
+ export type StoryVerdict = "NAILED" | "STRONG" | "WEAK" | "NONE" | "INVERSE";
1644
+
1645
+ /** A where-clause value: plain equality, or an explicit comparison. */
1646
+ export type StoryWhereValue =
1647
+ | string | number | boolean | null
1648
+ | { op: "==" | "!=" | ">=" | "<=" | ">" | "<"; value: string | number | boolean | null };
1649
+
1650
+ /** Named row-set over the breakdown result rows (all clauses AND together). */
1651
+ export interface StorySelect {
1652
+ [name: string]: { where: Record<string, StoryWhereValue> };
1653
+ }
1654
+
1655
+ /**
1656
+ * The pinned expect grammar. `metric` is one of exactly three forms: a single
1657
+ * ref `'<name>.<column>'`, a ratio `'<a>.<col> / <b>.<col>'`, or a difference
1658
+ * `'<a>.<col> - <b>.<col>'`. At most one binary operator; operands are refs or
1659
+ * numeric literals (at least one ref). If a selection matches multiple rows,
1660
+ * count-like columns sum and value-like columns error. No free-form
1661
+ * expressions.
1662
+ */
1663
+ export interface StoryExpect {
1664
+ metric: string;
1665
+ op: ">=" | "<=" | ">" | "<" | "between";
1666
+ /** The designed value — derive from the same exported knob constants the hook uses. `[lo, hi]` only with op `'between'`. */
1667
+ target: number | [number, number];
1668
+ /** Optional STRONG bound. Omitted → `target` doubles as the floor. */
1669
+ floor?: number;
1670
+ }
1671
+
1672
+ /** One mechanical assertion inside a story. Requires `expect` or `assert`. */
1673
+ export interface StoryAssertion {
1674
+ /**
1675
+ * Byte-compatible with `emulateBreakdown` / `verifyDungeon` args — or the
1676
+ * `{ type: 'duckdb', sql }` escape hatch (disk mode only; `{{PREFIX}}` in
1677
+ * the SQL is substituted with the run's data prefix path).
1678
+ */
1679
+ breakdown: Record<string, unknown> & { type: string; sql?: string };
1680
+ select?: StorySelect;
1681
+ expect?: StoryExpect;
1682
+ /**
1683
+ * JS-only escape hatch — discouraged; every use requires a comment
1684
+ * justifying why the declarative grammar can't express it. Return
1685
+ * `verdict` to place the result on the five-tier scale; otherwise
1686
+ * `pass` maps to STRONG / NONE.
1687
+ */
1688
+ assert?: (rows: Array<Record<string, unknown>>, ctx?: Record<string, unknown>) => { pass: boolean; detail?: string; verdict?: StoryVerdict };
1689
+ /** Population floor — below this, the verdict caps at WEAK. */
1690
+ minCohort?: number;
1691
+ }
1692
+
1693
+ /**
1694
+ * One story of the `stories` named export of a JS dungeon file (v1.6
1695
+ * story-spec). Living in the dungeon file means thresholds are computed from
1696
+ * the same knob constants the hook uses — the assertion can't drift from the
1697
+ * mechanism. Extra exports are ignored by `dungeon-loader` (zero compat
1698
+ * impact); `dungeon-to-json` drops them, so stories are JS-dungeon-only.
1699
+ * Schema: `lib/templates/story-spec.schema.json`.
1700
+ */
1701
+ export interface DungeonStory {
1702
+ /** Unique story id, conventionally `<hook>-<slug>` (e.g. `'H3-fraud-bursts'`). */
1703
+ id: string;
1704
+ /** Numbered hook this story verifies — matches the HOOK STORIES doc block (`'H3'` or `'Hook 3'`). */
1705
+ hook: string;
1706
+ archetype: StoryArchetype;
1707
+ /** One-to-three sentence human story the data tells. */
1708
+ narrative: string;
1709
+ /** Free-form report pointer for humans and /create-project — not interpreted by the runner. */
1710
+ mixpanelReport?: Record<string, unknown>;
1711
+ assertions: StoryAssertion[];
1712
+ /** Documented strict-bar / limitation notes. */
1713
+ intentionalDeviations?: string[];
1714
+ }
1715
+
1584
1716
  // ============= Text Generator Types =============
1585
1717
 
1586
1718
  /**
@@ -1911,14 +2043,39 @@ declare module '@ak--47/dungeon-master/hook-helpers' {
1911
2043
  export function injectBurst(events: EventSchema[], templateEvent: EventSchema, count: number, anchorTime: number | string, spreadMs: number): void;
1912
2044
  export function isPreAuthEvent(event: EventSchema, authTime: number | null): boolean;
1913
2045
  export function splitByAuth(events: EventSchema[], authTime: number | null): { preAuth: EventSchema[]; postAuth: EventSchema[]; stitch: EventSchema | null };
2046
+ /** v1.6.0 — deterministic FNV-1a hash of any id to [0, 1). Seed-independent and stable across runs. */
2047
+ export function hashFloat(id: string): number;
2048
+ /** v1.6.0 — deterministic cohort gate: true for `pct`% of ids (0-100 scale). Nest by hashing derived ids (e.g. `uid + ':wave2'`). */
2049
+ export function hashCohort(id: string, pct: number): boolean;
2050
+ /** Clone a template event of `eventName` onto `targetDays − existing` random inactive days within the user's active window. Schema-first: no template → no-op. */
2051
+ export function injectOnNewDays(events: EventSchema[], eventName: string, targetDays: number, options?: { timeRange?: 'active'; overrides?: Partial<EventSchema> }): EventSchema[];
2052
+ /** v1.6.0 — carve a dormant window (drop value moments, or all events with `dropAll`) then append a resurrection burst cloned from the surviving value-moment template. Returns a NEW array. */
2053
+ export function applyLifecycleWave(events: EventSchema[], uid: string, opts: { dormantFromDay: number; dormantDays: number; valueMomentEvent: string; resurrectBurst?: number; dropAll?: boolean }): EventSchema[];
2054
+ /** v1.6.0 — inject an ordered event path after each anchor for a deterministic `share` of users (hash-gated). Augments in place; engine auto-sort handles ordering. */
2055
+ export function applyPathBias(events: EventSchema[], uid: string, opts: { anchor: string; path: string[]; share: number; gapSeconds?: [number, number] }): EventSchema[];
2056
+ /** v1.6.0 — rewrite the user's timestamps into deterministic session clusters (n/week, m events, bounded span) that survive query-time re-derivation. */
2057
+ export function applySessionShape(events: EventSchema[], uid: string, opts: { sessionsPerWeek: number; eventsPerSession: number; sessionMinutes: number }): EventSchema[];
1914
2058
  }
1915
2059
 
1916
2060
  declare module '@ak--47/dungeon-master/hook-patterns' {
1917
- export function applyFrequencyByFrequency(events: EventSchema[], profile: Record<string, unknown> | null, opts: { cohortEvent: string; bins: Record<string, [number, number]>; targetEvent: string; multipliers: Record<string, number> }): void;
1918
- export function applyFunnelFrequencyBreakdown(allUserEvents: EventSchema[], profile: Record<string, unknown> | null, funnelEvents: EventSchema[], opts: { cohortEvent: string; bins: Record<string, [number, number]>; dropMultipliers: Record<string, number> }): void;
1919
- export function applyAggregateByBin(events: EventSchema[], profile: Record<string, unknown> | null, opts: { cohortEvent: string; bins: Record<string, [number, number]>; event: string; propertyName: string; deltas: Record<string, number> }): void;
2061
+ export function applyFrequencyByFrequency(events: EventSchema[], profile: Record<string, unknown> | null, opts: { cohortEvent: string; bins: Record<string, [number, number]>; targetEvent: string; multipliers: Record<string, number>; binBy?: 'events' | 'distinctDays' }): { bin: string | null; delta: number };
2062
+ export function applyFunnelFrequencyBreakdown(allUserEvents: EventSchema[], profile: Record<string, unknown> | null, funnelEvents: EventSchema[], opts: { cohortEvent: string; bins: Record<string, [number, number]>; dropMultipliers: Record<string, number>; finalStep?: string; binBy?: 'events' | 'distinctDays' }): { bin: string | null; droppedFinal: boolean };
2063
+ export function applyAggregateByBin(events: EventSchema[], profile: Record<string, unknown> | null, opts: { cohortEvent: string; bins: Record<string, [number, number]>; event: string; propertyName: string; deltas: Record<string, number>; binBy?: 'events' | 'distinctDays' }): { bin: string | null; scaled: number };
2064
+ /**
2065
+ * @deprecated v1.6.0 — the funnel-post variant only reaches the Mixpanel TTC
2066
+ * report for `isFirstFunnel` runs (TTC measures each user's FIRST occurrence
2067
+ * of the step sequence). Use `applyTTCBySegmentV2` from the `everything` hook.
2068
+ * Still works; warns once per process.
2069
+ */
1920
2070
  export function applyTTCBySegment(funnelEvents: EventSchema[], profile: Record<string, unknown>, opts: { segmentKey: string; factors: Record<string, number> }): void;
1921
- export function applyAttributedBySource(events: EventSchema[], profile: Record<string, unknown> | null, opts: { sourceEvent: string; sourceProperty: string; downstreamEvent: string; weights: Record<string, number>; model?: 'firstTouch' | 'lastTouch' }): void;
2071
+ /** v1.6.0 everything-hook TTC scaling of the user's FIRST occurrence of `steps` (what Mixpanel's TTC report measures). */
2072
+ export function applyTTCBySegmentV2(events: EventSchema[], profile: Record<string, unknown>, opts: { segmentKey: string; factors: Record<string, number>; steps: string[]; maxGapMinutes?: number }): { segmentValue: string | null; factor: number; shifted: number };
2073
+ /**
2074
+ * v1.6.0 rewrite — overwrites ENGINE-STAMPED touch values (the user's lifetime-first
2075
+ * and/or -last stamped `property` event) with a weighted draw, instead of stamping
2076
+ * fresh values that land outside the touchpoint cap's lookback.
2077
+ */
2078
+ export function applyAttributedBySource(events: EventSchema[], profile: Record<string, unknown> | null, opts: { weights: Record<string, number>; property?: string; model?: 'firstTouch' | 'lastTouch' | 'both' }): { overwritten: number; touches: number };
1922
2079
  }
1923
2080
 
1924
2081
  /**
@@ -1931,9 +2088,16 @@ declare module '@ak--47/dungeon-master/hook-patterns' {
1931
2088
  * | `aggregatePerUser` | `event`, `property`, `breakdownByFrequencyOf` | `agg` (default: `'avg'`) |
1932
2089
  * | `timeToConvert` | `fromEvent`, `toEvent`, `breakdownByUserProperty`, `profiles` | — |
1933
2090
  * | `attributedBy` | `conversionEvent`, `attributionEvent`, `attributionProperty` | `model` (default: `'lastTouch'`) |
2091
+ * | `sessionMetrics` | — | `event`, `metrics`, `source` (default: `'derived'`) |
2092
+ * | `retention` | `cohortEvent`, `returnEvent` (or `compounded`) | see `RetentionConfig` |
2093
+ * | `distinctCount` | `event`, `property` | `topN` |
2094
+ * | `eventBreakdown` (v1.6) | `breakdownProperty` | `event`, `countType`, `topN`, `firstTimeOnly` |
2095
+ * | `uniques` (v1.6) | `event` | `unit`, `rollingWindow`, `cumulative`, `countType`, `firstTimeOnly` |
2096
+ * | `lifecycle` (v1.6) | `valueMomentEvent` | `periodDays` (7 or 30) |
2097
+ * | `topPaths` (v1.6) | `anchors` | `forward`, `reverse`, `countType`, `hiddenEvents`, `visibleEvents`, `output` |
1934
2098
  */
1935
2099
  export interface EmulateOptions {
1936
- type: 'frequencyByFrequency' | 'funnelFrequency' | 'aggregatePerUser' | 'timeToConvert' | 'attributedBy' | 'sessionMetrics' | 'retention' | 'distinctCount';
2100
+ type: 'frequencyByFrequency' | 'funnelFrequency' | 'aggregatePerUser' | 'timeToConvert' | 'attributedBy' | 'sessionMetrics' | 'retention' | 'distinctCount' | 'eventBreakdown' | 'uniques' | 'lifecycle' | 'topPaths';
1937
2101
  metricEvent?: string;
1938
2102
  breakdownByFrequencyOf?: string;
1939
2103
  perUser?: boolean;
@@ -1977,40 +2141,121 @@ export interface EmulateOptions {
1977
2141
  * `period`; empty rows have ONLY `period` and `_empty: true`.
1978
2142
  */
1979
2143
  timeBucketRange?: { from: number | string; to: number | string };
1980
- /** v1.5.0 — sessionMetrics: filter to sessions containing this event. Omit for all sessions. */
2144
+ /** sessionMetrics: which metrics to compute. Default: `['count', 'duration', 'eventsPerSession']`. (The session filter to a containing event is the `event` field.) */
1981
2145
  metrics?: Array<'count' | 'duration' | 'eventsPerSession'>;
1982
-
1983
- // v1.5.0 retention extensions
1984
- /** Retention birth event name. */
1985
- cohortEvent?: string;
1986
- /** Retention return event name. */
1987
- returnEvent?: string;
2146
+ /**
2147
+ * v1.6.0 sessionMetrics session source. `'derived'` (default) re-derives
2148
+ * sessions from raw timestamps via `sessionize()` — what Mixpanel actually
2149
+ * computes at query time. `'stamped'` restores the 1.5 reading of the
2150
+ * generator's pre-stamped `session_id`. Rows carry `stampedDivergence`
2151
+ * auditing the gap. Invalid values throw.
2152
+ */
2153
+ source?: 'derived' | 'stamped';
2154
+ /** v1.6.0 — session derivation timeout in ms (default 30 min). Used by sessionMetrics, `countType: 'sessions'`, and topPaths. */
2155
+ sessionTimeoutMs?: number;
2156
+ /** v1.6.0 — max session length in ms (default 24 h). */
2157
+ maxSessionMs?: number;
2158
+
2159
+ // v1.5.0 retention extensions (completed in v1.6.0 — unknown option keys THROW)
2160
+ /** Retention birth event name (`null` or `'$any_event'` = any event). */
2161
+ cohortEvent?: string | null;
2162
+ /** Retention return event name (`null` or `'$any_event'` = any event). Forbidden with `compounded: true`. */
2163
+ returnEvent?: string | null;
1988
2164
  /** Day buckets to check (offsets from birth, ≥1). */
1989
2165
  dayBuckets?: number[];
1990
- /** Segment cohort by birth event property value (Mixpanel segment_event=FIRST). */
2166
+ /** Segment by event property value (birth event by default; see `segmentOn`). */
1991
2167
  segmentBy?: string;
1992
- /** CARRY_FORWARD unbounded mode — once retained, counted on all later buckets. */
2168
+ /**
2169
+ * v1.6.0 — property filter on the birth event. Map of prop → value (shorthand
2170
+ * for `eq`) or `{ op, value }` with the shared WHERE rulebook ops
2171
+ * (`eq`/`neq`/`gt`/`lt`/`gte`/`lte`/`contains`/`not_contains`).
2172
+ */
2173
+ cohortWhere?: Record<string, unknown>;
2174
+ /** v1.6.0 — property filter on the return event (same shape as `cohortWhere`). Forbidden with `compounded: true`. */
2175
+ returnWhere?: Record<string, unknown>;
2176
+ /** v1.6.0 — compounded retention: the return side is the cohort side (event AND filters — retention_query.cpp:677-685). */
2177
+ compounded?: boolean;
2178
+ /** v1.6.0 — bucket width. `month` is 31 FIXED days, not calendar months (libquery/util.h:265-273). Default `'day'`. */
2179
+ bucketUnit?: 'hour' | 'day' | 'week' | 'month';
2180
+ /** v1.6.0 — unbounded retention mode. Default `'none'`. */
2181
+ unbounded?: 'none' | 'carryForward' | 'carryBack' | 'consecutiveForward';
2182
+ /** @deprecated v1.6.0 — boolean alias for `unbounded: 'carryForward'`. Use `unbounded`. */
1993
2183
  carry_forward?: boolean;
2184
+ /** v1.6.0 — birth-time alignment. `'calendarStart'` floors the birth to the bucket-unit boundary (ISO Monday for weeks). Default `'birth'`. */
2185
+ bucketAlignment?: 'birth' | 'calendarStart';
2186
+ /** v1.6.0 — restrict births to this inclusive ms/ISO window. */
2187
+ cohortWindow?: { from?: number | string; to?: number | string };
2188
+ /** v1.6.0 — which event `segmentBy` reads. `'return'` = SEGMENT_EVENT_SECOND (a user can appear in multiple segments). Default `'birth'`. */
2189
+ segmentOn?: 'birth' | 'return';
1994
2190
  /**
1995
2191
  * v1.5.0 — Mixpanel `birth_can_retain` (retention_query.cpp:1097-1109). Default
1996
2192
  * `false`: a return event at the EXACT birth ms is NOT counted (strict `<`).
1997
- * Set `true` to count exact-birth-ms returns (rare; usually a same-event-as-birth
1998
- * pattern requires COMPOUNDED retention which is not supported here).
2193
+ * Set `true` to count exact-birth-ms returns. For same-event-as-birth patterns
2194
+ * see `compounded: true` (supported since v1.6.0).
1999
2195
  */
2000
2196
  birthCanRetain?: boolean;
2001
2197
 
2002
2198
  // v1.5.1 distinctCount extensions
2003
- /** Optional cap on the number of top-N values returned in `top_values`. Defaults to 25 (matches Mixpanel UI). */
2199
+ /** distinctCount: cap on `top_values` (default 25, matches Mixpanel UI). eventBreakdown: segment cap (default 250, no "other" bucket). */
2004
2200
  topN?: number;
2201
+
2202
+ // v1.6.0 eventBreakdown / uniques / lifecycle / topPaths extensions
2203
+ /** eventBreakdown + topPaths: event property to segment by (lists fan out per item; `null`/`undefined` → the "undefined" bucket). */
2204
+ breakdownProperty?: string;
2205
+ /**
2206
+ * Count type. eventBreakdown: `'general'` (events, default), `'unique'`
2207
+ * (distinct users), `'sessions'` (once per user-session per segment) —
2208
+ * unrecognized values throw. uniques: `'sessions'` counts distinct
2209
+ * (user, session) pairs per bucket. topPaths: `'unique' | 'general' | 'sessions'`.
2210
+ */
2211
+ countType?: 'general' | 'unique' | 'sessions';
2212
+ /** v1.6.0 — restrict to each user's first-ever occurrence of `event` before counting (eventBreakdown + uniques). */
2213
+ firstTimeOnly?: boolean;
2214
+ /** uniques: bucket unit (default `'day'`; `'range'` = one bucket over the whole window). */
2215
+ unit?: 'hour' | 'day' | 'week' | 'month' | 'range';
2216
+ /** uniques: rolling look-back window in days (WAU=7, MAU=30). An event on day E lands the user in buckets [E, E+W−1]. */
2217
+ rollingWindow?: number;
2218
+ /** uniques: each bucket reports the running distinct-user count through that bucket. Not composable with `countType: 'sessions'`. */
2219
+ cumulative?: boolean;
2220
+ /** uniques: property filter on counted events (same shape as `cohortWhere`). */
2221
+ where?: Record<string, unknown>;
2222
+ /** lifecycle: the value-moment event classified into new/retained/resurrected/dormant. */
2223
+ valueMomentEvent?: string;
2224
+ /** lifecycle: period length — 7 or 30 (the LCA board-template variants). Default 7. */
2225
+ periodDays?: 7 | 30;
2226
+ /** topPaths: anchor event name(s) paths start from. */
2227
+ anchors?: string[];
2228
+ /** topPaths: steps captured after the anchor (default 2, ARB cap 10). */
2229
+ forward?: number;
2230
+ /** topPaths: steps captured before the anchor (default 0). */
2231
+ reverse?: number;
2232
+ /** topPaths: events removed from streams before matching. */
2233
+ hiddenEvents?: string[];
2234
+ /** topPaths: when set, ONLY these events (plus anchors) are visible. */
2235
+ visibleEvents?: string[];
2236
+ /** topPaths: collapse immediate repeats of the same event (default true). */
2237
+ collapseRepeated?: boolean;
2238
+ /** topPaths: output shape — `'list'` (ARB Top Paths leaf rows, top-50) or `'sankey'`. Default `'list'`. */
2239
+ output?: 'list' | 'sankey';
2240
+ /** topPaths: per-level top-N pruning threshold into `$mp_uncommon_flows_events`. */
2241
+ cardinalityThreshold?: number;
2005
2242
  }
2006
2243
 
2007
- /** v1.5.0 — config for `emulateBreakdown({ type: 'retention' })`. */
2244
+ /**
2245
+ * v1.5.0 — config for `emulateBreakdown({ type: 'retention' })`. Completed in
2246
+ * v1.6.0: `compounded`, `cohortWhere`/`returnWhere`, `bucketUnit`, `unbounded`
2247
+ * (carryForward/carryBack/consecutiveForward), `bucketAlignment: 'calendarStart'`,
2248
+ * `cohortWindow`, `segmentOn: 'return'`. Unknown option keys THROW (v1.6.0
2249
+ * behavior change — a typo'd option no longer runs silently with defaults).
2250
+ */
2008
2251
  export interface RetentionConfig extends EmulateOptions {
2009
2252
  type: 'retention';
2010
- cohortEvent: string;
2011
- returnEvent: string;
2012
- dayBuckets: number[];
2253
+ cohortEvent: string | null;
2254
+ /** Required unless `compounded: true` (which sets the return side := cohort side). */
2255
+ returnEvent?: string | null;
2256
+ dayBuckets?: number[];
2013
2257
  segmentBy?: string;
2258
+ /** @deprecated v1.6.0 — use `unbounded: 'carryForward'`. */
2014
2259
  carry_forward?: boolean;
2015
2260
  }
2016
2261
 
@@ -2021,6 +2266,8 @@ export interface SessionMetricsConfig extends EmulateOptions {
2021
2266
  event?: string;
2022
2267
  /** Which metrics to compute. Default: `['count', 'duration', 'eventsPerSession']`. */
2023
2268
  metrics?: Array<'count' | 'duration' | 'eventsPerSession'>;
2269
+ /** v1.6.0 — `'derived'` (default) re-derives sessions from timestamps; `'stamped'` reads pre-stamped `session_id` (the 1.5 behavior). */
2270
+ source?: 'derived' | 'stamped';
2024
2271
  }
2025
2272
 
2026
2273
  declare module '@ak--47/dungeon-master/verify' {
@@ -2040,11 +2287,30 @@ declare module '@ak--47/dungeon-master/verify' {
2040
2287
  /** Greedy single-pass funnel evaluator options. */
2041
2288
  export interface FunnelOptions {
2042
2289
  conversionWindowMs?: number;
2290
+ /**
2291
+ * v1.6.0 — session-count conversion window (`{ unit: 'sessions', n }`,
2292
+ * n ≤ 12): steps must land within n sessions of step 0 (ordinal-only
2293
+ * per-step check). Mutually exclusive with `conversionWindowMs`.
2294
+ */
2295
+ conversionWindow?: { unit: 'sessions'; n: number };
2043
2296
  graceperiod?: boolean;
2044
2297
  reentry?: boolean;
2045
2298
  exclusionSteps?: ExclusionStep[];
2046
2299
  trackStepProperties?: boolean | string[];
2047
- countMode?: 'uniques' | 'totals';
2300
+ /**
2301
+ * `'totals'` returns an ARRAY of FunnelResult — one per attempt.
2302
+ * v1.6.0 `'sessions'` is Mixpanel's "count by Sessions" API-rewrite
2303
+ * preset: expands to `totals` + `woRepeat` + conversion window of 1
2304
+ * session; throws when combined with `reentry` or another window.
2305
+ */
2306
+ countMode?: 'uniques' | 'totals' | 'sessions';
2307
+ /**
2308
+ * v1.6.0 — COUNT_TYPE_GENERAL_WO_REPEAT: totals counting where window
2309
+ * expiry is the ONLY restart (at most one attempt per window span).
2310
+ * Requires `countMode: 'totals'`; mutually exclusive with `reentry`
2311
+ * and `sessionScoped`.
2312
+ */
2313
+ woRepeat?: boolean;
2048
2314
  sessionScoped?: boolean;
2049
2315
  }
2050
2316
  /** Per-attempt funnel result. */