@ak--47/dungeon-master 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +30 -11
  2. package/.claude/skills/create-dungeon/SKILL.md +84 -44
  3. package/.claude/skills/create-project/SKILL.md +28 -3
  4. package/.claude/skills/create-project/context.mjs +89 -0
  5. package/.claude/skills/create-project/provision.mjs +1 -60
  6. package/.claude/skills/headless-build/SKILL.md +39 -12
  7. package/.claude/skills/powertools/SKILL.md +26 -3
  8. package/.claude/skills/release-check/SKILL.md +124 -0
  9. package/.claude/skills/verify-dungeon/SKILL.md +103 -29
  10. package/.claude/skills/verify-dungeon/references/alignment-contract.md +84 -0
  11. package/.claude/skills/verify-dungeon/references/counting-semantics.md +41 -16
  12. package/.claude/skills/verify-dungeon/references/report-format.md +41 -10
  13. package/.claude/skills/verify-dungeon/references/sql-recipes.md +171 -226
  14. package/.claude/skills/warehouse-metrics/GAPS-template.md +34 -0
  15. package/.claude/skills/warehouse-metrics/SKILL.md +111 -0
  16. package/.claude/skills/warehouse-metrics/deploy.mjs +651 -0
  17. package/.claude/skills/write-hooks/SKILL.md +94 -51
  18. package/CHANGELOG.md +183 -0
  19. package/HOOKS.md +165 -18
  20. package/README.md +265 -1
  21. package/docs/guides/1.8.0-upgrade-guide.md +151 -0
  22. package/docs/guides/1.8.1-upgrade-guide.md +153 -0
  23. package/dungeons/technical/warehouse.js +187 -0
  24. package/index.js +116 -2
  25. package/lib/core/config-validator.js +21 -0
  26. package/lib/core/dungeon-loader.js +1 -1
  27. package/lib/core/storage.js +51 -3
  28. package/lib/generators/events.js +6 -0
  29. package/lib/generators/funnels.js +15 -0
  30. package/lib/generators/standalone.js +248 -0
  31. package/lib/generators/warehouse.js +828 -0
  32. package/lib/hook-helpers/shape.js +73 -17
  33. package/lib/orchestrators/mixpanel-sender.js +27 -2
  34. package/lib/orchestrators/user-loop.js +83 -15
  35. package/lib/templates/story-spec.schema.json +41 -16
  36. package/lib/utils/utils.js +37 -12
  37. package/lib/verify/funnel-engine.js +66 -26
  38. package/lib/verify/index.js +1 -0
  39. package/lib/verify/story-runner.js +71 -8
  40. package/lib/verify/warehouse.js +683 -0
  41. package/package.json +4 -2
  42. package/scripts/verify-stories.mjs +150 -44
  43. package/types.d.ts +312 -9
package/types.d.ts CHANGED
@@ -453,6 +453,18 @@ export interface Dungeon {
453
453
  groupProps?: Record<string, Record<string, ValueValid>>;
454
454
  /** Lookup table definitions for dimension tables. */
455
455
  lookupTables?: LookupTableSchema[];
456
+ /**
457
+ * v1.8.0 — identity-less metric snapshots. One record per cadence tick per
458
+ * dimension cross-product row, carrying NO `user_id` and NO `device_id`.
459
+ *
460
+ * Use for infrastructure and finance telemetry: daily CDN egress per region,
461
+ * weekly billing rollups per plan tier, hourly queue depth per cluster.
462
+ * `$ad_spend` (`hasAdSpend: true`) is the same idea hard-coded; this is the
463
+ * general form and it does not use a Mixpanel reserved event name.
464
+ */
465
+ standaloneEvents?: StandaloneEventConfig[];
466
+ /** v1.8.0 — warehouse-backed metric source tables derived from the run's own event stream. */
467
+ warehouseMetrics?: WarehouseMetricConfig[];
456
468
  /** TimeSoup configuration: shapes intra-week and intra-day rhythm (peaks, deviation, DOW/HOD weights). Pair with `macro` for big-picture trend control. */
457
469
  soup?: soup;
458
470
  /** Macro trend shape across the full dataset window: birth distribution + per-user event allocation. Default: "flat". Use "growth"/"viral"/"steady"/"decline" or a custom object. */
@@ -703,7 +715,7 @@ export interface ResolvedMacro {
703
715
  * - "everything" — array of ALL events for one user (return array to replace; meta.profile available)
704
716
  *
705
717
  * Storage-only hooks (fire during hookPush, not in generators):
706
- * - "ad-spend", "group", "mirror", "lookup"
718
+ * - "ad-spend", "group", "mirror", "lookup", "standalone", "warehouse"
707
719
  */
708
720
  export type hookTypes =
709
721
  | "event"
@@ -716,6 +728,8 @@ export type hookTypes =
716
728
  | "funnel-pre"
717
729
  | "funnel-post"
718
730
  | "ad-spend"
731
+ | "standalone"
732
+ | "warehouse"
719
733
  | "churn"
720
734
  | "group-event"
721
735
  | "everything"
@@ -732,7 +746,9 @@ export type hookTypes =
732
746
  * - "event": return value REPLACES the event (must be the event object).
733
747
  * - "everything": return an array to REPLACE the user's event list (filter/inject/dedupe).
734
748
  * - "user", "scd-pre", "funnel-pre", "funnel-post": return value is IGNORED — mutate in place.
735
- * - storage-only ("ad-spend", "group", "mirror", "lookup"): return value is IGNORED.
749
+ * - storage-only ("ad-spend", "group", "mirror", "lookup", "standalone"): return an object or array of records; undefined drops the record.
750
+ * - "warehouse": return value is IGNORED; mutate the row in place.
751
+ * - "standalone" runs before the user loop; "warehouse" runs after it. Neither receives person metadata or enters "everything".
736
752
  *
737
753
  * @param record - The data being processed (event, profile, array of events, funnel config, etc.).
738
754
  * @param type - Which hook type is firing — see `hookTypes`.
@@ -908,6 +924,10 @@ export interface hookArrayOptions<T> {
908
924
  concurrency?: number;
909
925
  /** Generation context (config, runtime, defaults). */
910
926
  context?: Context;
927
+ /** Warehouse metric name for warehouse containers. */
928
+ metricName?: string;
929
+ /** Fixed CSV column order for warehouse metric tables. */
930
+ fixedColumns?: string[];
911
931
  }
912
932
 
913
933
  /**
@@ -929,6 +949,10 @@ export interface HookedArray<T> extends Array<T> {
929
949
  getWritePath: () => string;
930
950
  /** Returns all file paths written by this container during the current run. */
931
951
  getWrittenFiles: () => string[];
952
+ /** Storage hook type this array is configured for. */
953
+ type?: hookTypes | string;
954
+ /** Output serialization format for this array. */
955
+ format?: string;
932
956
  /** SCD prop name this array carries (only set on SCD HookedArrays). */
933
957
  scdKey?: string;
934
958
  /** Entity type for SCDs ("user" or a group key). */
@@ -937,6 +961,10 @@ export interface HookedArray<T> extends Array<T> {
937
961
  groupKey?: string;
938
962
  /** Lookup table key this array carries (only set on lookup table HookedArrays). */
939
963
  lookupKey?: string;
964
+ /** Warehouse metric name this array carries (only set on warehouse HookedArrays). */
965
+ metricName?: string;
966
+ /** Fixed CSV column order for warehouse metric tables. */
967
+ fixedColumns?: string[];
940
968
  }
941
969
 
942
970
  export type AllData =
@@ -954,8 +982,11 @@ export interface Storage {
954
982
  mirrorEventData?: HookedArray<EventSchema>;
955
983
  userProfilesData?: HookedArray<UserProfile>;
956
984
  adSpendData?: HookedArray<EventSchema>;
985
+ standaloneEventData?: HookedArray<EventSchema>;
957
986
  groupProfilesData?: HookedArray<GroupProfileSchema>[];
958
987
  lookupTableData?: HookedArray<LookupTableSchema>[];
988
+ warehouseMetricData?: HookedArray<Record<string, any>>[];
989
+ warehouseManifestFile?: string;
959
990
  scdTableData?: HookedArray<SCDSchema>[];
960
991
  groupEventData?: HookedArray<EventSchema>;
961
992
  }
@@ -1023,6 +1054,14 @@ export interface Context {
1023
1054
  FIXED_NOW: number;
1024
1055
  /** Start of the resolved dataset window (unix seconds). Equal to the user-supplied `datasetStart`, or fallback `today_start - numDays`. */
1025
1056
  FIXED_BEGIN?: number;
1057
+ /** Runtime accumulator for post-loop warehouse metric materialization. */
1058
+ warehouseAccumulator?: {
1059
+ warnings?: string[];
1060
+ ingest: (events: EventSchema[]) => void;
1061
+ getCell: (metricName: string, seriesKey: string, bucketStartSec: number) => any;
1062
+ };
1063
+ /** Manifest describing materialized warehouse tables for downstream tooling. */
1064
+ warehouseManifest?: WarehouseManifest;
1026
1065
  /** Alias of `FIXED_BEGIN` — surfaced on hook `meta.datasetStart`. */
1027
1066
  DATASET_START_SECONDS: number;
1028
1067
  /** Alias of `FIXED_NOW` — surfaced on hook `meta.datasetEnd`. */
@@ -1596,6 +1635,12 @@ export type Result = {
1596
1635
  scdTableData: SCDSchema[][];
1597
1636
  /** Ad-spend events (only populated when `hasAdSpend: true`). */
1598
1637
  adSpendData: EventSchema[];
1638
+ /** Identity-less metric snapshots (only populated when `standaloneEvents` is set). v1.8.0. */
1639
+ standaloneEventData: EventSchema[];
1640
+ /** Materialized warehouse metric tables keyed by metric name. */
1641
+ warehouseMetricData: Record<string, Record<string, any>[]>;
1642
+ /** Warehouse table manifest surfaced whenever `warehouseMetrics` is configured. */
1643
+ warehouseManifest?: WarehouseManifest;
1599
1644
  /** Group profiles — one inner array per group key. */
1600
1645
  groupProfilesData: GroupProfileSchema[][];
1601
1646
  /** Lookup tables — one inner array per table. */
@@ -2007,9 +2052,10 @@ export interface StoryAssertion {
2007
2052
  /**
2008
2053
  * Byte-compatible with `emulateBreakdown` / `verifyDungeon` args — or the
2009
2054
  * `{ type: 'duckdb', sql }` escape hatch (disk mode only; `{{PREFIX}}` in
2010
- * the SQL is substituted with the run's data prefix path).
2055
+ * the SQL is substituted with the run's data prefix path), or warehouse
2056
+ * verification rows via `{ type: 'warehouse' | 'warehouse-stats', table }`.
2011
2057
  */
2012
- breakdown: Record<string, unknown> & { type: string; sql?: string };
2058
+ breakdown: Record<string, unknown> & { type: string; sql?: string; table?: string };
2013
2059
  select?: StorySelect;
2014
2060
  expect?: StoryExpect;
2015
2061
  /**
@@ -2325,13 +2371,266 @@ export interface WritePaths {
2325
2371
  eventFiles: string[];
2326
2372
  userFiles: string[];
2327
2373
  adSpendFiles: string[];
2374
+ standaloneFiles: string[];
2328
2375
  scdFiles: string[];
2329
2376
  mirrorFiles: string[];
2330
2377
  groupFiles: string[];
2331
2378
  lookupFiles: string[];
2379
+ warehouseFiles: string[];
2332
2380
  folder: string;
2333
2381
  }
2334
2382
 
2383
+ // ============= Standalone (identity-less) Events — v1.8.0 =============
2384
+
2385
+ /**
2386
+ * An identity-less metric snapshot stream.
2387
+ *
2388
+ * The engine emits one record per cadence tick per dimension cross-product row.
2389
+ * Records carry `event`, `time`, `insert_id`, `distinct_id`, every dimension as
2390
+ * a flat property, and every resolved entry in `properties`. They never carry
2391
+ * `user_id` or `device_id`, because they describe a system, not a person.
2392
+ *
2393
+ * @example
2394
+ * standaloneEvents: [{
2395
+ * event: 'cdn_egress',
2396
+ * cadence: 'day',
2397
+ * dimensions: { region: ['us-east', 'us-west', 'eu', 'apac'] },
2398
+ * distinctIdFrom: 'region',
2399
+ * properties: {
2400
+ * gb_out: (ctx) => 400 + ctx.tickIndex * 3,
2401
+ * cost_usd: (ctx) => (400 + ctx.tickIndex * 3) * 0.085,
2402
+ * p95_ms: [120, 140, 160],
2403
+ * },
2404
+ * }]
2405
+ */
2406
+ export interface StandaloneEventConfig {
2407
+ /** Event name as it lands in Mixpanel. Must be unique across `standaloneEvents`. */
2408
+ event: string;
2409
+ /**
2410
+ * How often a snapshot fires. Ticks start at the dataset start and step by
2411
+ * the cadence; the last tick is the final one at or before the dataset end.
2412
+ * Default: `'day'`.
2413
+ */
2414
+ cadence?: 'hour' | 'day' | 'week';
2415
+ /**
2416
+ * Dimension values to cross-product. Each key becomes a flat property on the
2417
+ * record. `{ region: ['us','eu'], tier: ['a','b'] }` emits 4 records per tick.
2418
+ * Omit for a single record per tick.
2419
+ */
2420
+ dimensions?: Record<string, any[]>;
2421
+ /**
2422
+ * Which dimension supplies the synthetic `distinct_id`. Must name a declared
2423
+ * dimension. When omitted, `distinct_id` is the event name. The id exists so
2424
+ * Mixpanel accepts the record; it never maps to a person.
2425
+ */
2426
+ distinctIdFrom?: string;
2427
+ /**
2428
+ * Snapshot metrics. Same `ValueValid` forms as event properties, and value
2429
+ * functions receive a `StandaloneValueContext` so a metric can shape a trend
2430
+ * across the window.
2431
+ */
2432
+ properties?: Record<string, ValueValid>;
2433
+ }
2434
+
2435
+ /** @internal Normalized `StandaloneEventConfig` produced by the validator. */
2436
+ export interface ResolvedStandaloneEventConfig {
2437
+ event: string;
2438
+ cadence: 'hour' | 'day' | 'week';
2439
+ dimensions: Record<string, any[]>;
2440
+ distinctIdFrom: string | null;
2441
+ properties: Record<string, ValueValid>;
2442
+ }
2443
+
2444
+ /**
2445
+ * Context handed to every standalone property value function.
2446
+ * Shares `time` and `config` with `ValueContext`, so a function written for a
2447
+ * normal event property still works unchanged.
2448
+ */
2449
+ export interface StandaloneValueContext {
2450
+ /** Tick timestamp in unix MILLISECONDS. */
2451
+ time: number;
2452
+ /** The full validated dungeon config. */
2453
+ config: Dungeon;
2454
+ /** This row's dimension values, e.g. `{ region: 'us-east' }`. */
2455
+ dimensions: Record<string, any>;
2456
+ /** Zero-based index of this tick within the window. Use it to shape a trend. */
2457
+ tickIndex: number;
2458
+ /** Total number of ticks in the window. `tickIndex / (tickCount - 1)` is window progress. */
2459
+ tickCount: number;
2460
+ /** The cadence this stream fires on. */
2461
+ cadence: 'hour' | 'day' | 'week';
2462
+ /** The partially built record (`event`, `time`, `insert_id`, `distinct_id`, dimensions). */
2463
+ event: Record<string, any>;
2464
+ }
2465
+
2466
+ /**
2467
+ * Meta passed to the `"standalone"` hook.
2468
+ *
2469
+ * Storage-only: return the record or an array of records to retain them.
2470
+ * Returning undefined drops the record. Warehouse hooks instead ignore returns.
2471
+ */
2472
+ export interface HookMetaStandalone extends HookMetaTimeAnchors {
2473
+ /** The resolved config for the stream this record belongs to. */
2474
+ spec: ResolvedStandaloneEventConfig;
2475
+ /** The full validated dungeon config. */
2476
+ config: Dungeon;
2477
+ }
2478
+
2479
+ export interface WarehouseMetricSource {
2480
+ /** Source event names whose bucketed measure contributes positively to the series. */
2481
+ event: string | string[];
2482
+ /** Source event names whose bucketed measure is subtracted from the series. */
2483
+ minus?: string | string[];
2484
+ /** Per-bucket measure. Default: `'count'`. */
2485
+ measure?: 'count' | 'sum' | 'avg' | 'dau' | 'users';
2486
+ /** Required when `measure` is `'sum'` or `'avg'`. */
2487
+ property?: string;
2488
+ /** Optional row filter over flat event records. */
2489
+ where?: ((event: Record<string, any>) => boolean) | null;
2490
+ /** Optional dimension columns copied from source event or super prop keys. */
2491
+ groupBy?: string | string[];
2492
+ }
2493
+
2494
+ export interface WarehouseMetricConfig {
2495
+ /**
2496
+ * @example
2497
+ * warehouseMetrics: [{
2498
+ * name: 'daily_active_subscriptions',
2499
+ * type: 'point-in-time',
2500
+ * source: {
2501
+ * event: 'subscription_started',
2502
+ * minus: 'subscription_cancelled',
2503
+ * measure: 'count',
2504
+ * },
2505
+ * baseline: 40,
2506
+ * timeColumn: 'date',
2507
+ * valueColumn: 'active_subscriptions',
2508
+ * }]
2509
+ */
2510
+ /** Unique metric/table name. Must match `/^[a-z][a-z0-9_]{0,63}$/`. */
2511
+ name: string;
2512
+ /** Metric family: additive sums per bucket vs point-in-time carried levels. Default: `'additive'`. */
2513
+ type?: 'additive' | 'point-in-time';
2514
+ /** Bucket grain. Default: `'day'`. */
2515
+ grain?: 'day' | 'week' | 'month';
2516
+ /** Point-in-time only: emit only the first bucket and changed values. Default: `false`. */
2517
+ sparse?: boolean;
2518
+ /** Declarative source spec describing how to derive the table from generated events. */
2519
+ source: WarehouseMetricSource;
2520
+ /** Output time column name. Default: `'date'`. */
2521
+ timeColumn?: string;
2522
+ /** Output value column name. Default: `'value'`. */
2523
+ valueColumn?: string;
2524
+ /** Point-in-time starting level at the dataset window start. Default: `0`. */
2525
+ baseline?: number;
2526
+ /** Multiplier applied after bucket aggregation. Default: `1`. */
2527
+ scale?: number;
2528
+ /** Seeded jitter fraction clamped to `[0, 0.5]`. Default: `0`. */
2529
+ noise?: number;
2530
+ /** Grain periods of backfill before the dataset window. Default: `0`. */
2531
+ history?: number;
2532
+ /** Extra declared output columns, preserved in declaration order. */
2533
+ columns?: Record<string, ValueValid | ((ctx: WarehouseValueContext) => ValueValid)>;
2534
+ /** Output file format. Defaults to the dungeon format, else `'csv'`. */
2535
+ format?: 'csv' | 'json';
2536
+ }
2537
+
2538
+ /** @internal Normalized `WarehouseMetricConfig` produced by the validator. */
2539
+ export interface ResolvedWarehouseMetricConfig {
2540
+ name: string;
2541
+ type: 'additive' | 'point-in-time';
2542
+ grain: 'day' | 'week' | 'month';
2543
+ sparse: boolean;
2544
+ source: {
2545
+ event: string[];
2546
+ minus: string[];
2547
+ measure: 'count' | 'sum' | 'avg' | 'dau' | 'users';
2548
+ property: string | null;
2549
+ where: ((event: Record<string, any>) => boolean) | null;
2550
+ groupBy: string[];
2551
+ };
2552
+ timeColumn: string;
2553
+ valueColumn: string;
2554
+ baseline: number;
2555
+ scale: number;
2556
+ noise: number;
2557
+ history: number;
2558
+ columns: Record<string, ValueValid | ((ctx: WarehouseValueContext) => ValueValid)>;
2559
+ format: 'csv' | 'json';
2560
+ }
2561
+
2562
+ export interface WarehouseValueContext {
2563
+ /** Final bucket value after scale and noise. */
2564
+ value: number;
2565
+ /** Partially built row so later columns can depend on earlier ones. */
2566
+ row: Record<string, any>;
2567
+ /** Bucket start in unix milliseconds. */
2568
+ time: number;
2569
+ /** Zero-based chronological bucket index within this series, including backfill buckets and sparse gaps when present. */
2570
+ bucketIndex: number;
2571
+ /** Total chronological buckets in this series, including history buckets even when sparse rows are skipped. */
2572
+ bucketCount: number;
2573
+ /** Bucket grain for this metric. */
2574
+ grain: 'day' | 'week' | 'month';
2575
+ /** True when this row was synthesized before the dataset window by `history`. */
2576
+ isBackfill: boolean;
2577
+ /** Stable joined dimension key for this series. Empty string when undimensioned. */
2578
+ seriesKey: string;
2579
+ /** The resolved metric spec for this table. */
2580
+ spec: ResolvedWarehouseMetricConfig;
2581
+ /** The full validated dungeon config. */
2582
+ config: Dungeon;
2583
+ }
2584
+
2585
+ export interface HookMetaWarehouse extends HookMetaTimeAnchors {
2586
+ /** The resolved config for the metric this row belongs to. */
2587
+ spec: ResolvedWarehouseMetricConfig;
2588
+ /** The full validated dungeon config. */
2589
+ config: Dungeon;
2590
+ /** Metric/table name. */
2591
+ metricName: string;
2592
+ /** Zero-based chronological bucket index within this series, including history buckets and sparse gaps. */
2593
+ bucketIndex: number;
2594
+ /** Total chronological buckets in this series, including history buckets even when sparse rows are skipped. */
2595
+ bucketCount: number;
2596
+ /** Bucket grain for the metric. */
2597
+ grain: 'day' | 'week' | 'month';
2598
+ /** Stable joined dimension key for this series. Empty string when undimensioned. */
2599
+ seriesKey: string;
2600
+ /** True when the row belongs to the `history` backfill before the dataset window. */
2601
+ isBackfill: boolean;
2602
+ /** Raw bucket contributions before scale/noise and before point-in-time carry-forward. */
2603
+ raw: {
2604
+ plus: { count: number; sum: number; users: number };
2605
+ minus: { count: number; sum: number; users: number };
2606
+ };
2607
+ }
2608
+
2609
+ export interface WarehouseManifestColumn {
2610
+ name: string;
2611
+ bqType: 'DATE' | 'FLOAT64' | 'BOOL' | 'STRING';
2612
+ }
2613
+
2614
+ export interface WarehouseManifestTable {
2615
+ table: string;
2616
+ file: string;
2617
+ format: 'csv' | 'json';
2618
+ grain: 'day' | 'week' | 'month';
2619
+ type: 'additive' | 'point-in-time';
2620
+ timeColumn: string;
2621
+ valueColumn: string;
2622
+ dimensionColumns: string[];
2623
+ columns: WarehouseManifestColumn[];
2624
+ recommendedAggregation: 'sum' | 'last value';
2625
+ sql: string;
2626
+ refreshHint: string;
2627
+ }
2628
+
2629
+ export interface WarehouseManifest {
2630
+ configName: string;
2631
+ tables: WarehouseManifestTable[];
2632
+ }
2633
+
2335
2634
  /**
2336
2635
  * Configuration for TimeSoup time distribution function
2337
2636
  */
@@ -2384,10 +2683,10 @@ declare module '@ak--47/dungeon-master/hook-helpers' {
2384
2683
  export function injectOnNewDays(events: EventSchema[], eventName: string, targetDays: number, options?: { timeRange?: 'active'; overrides?: Partial<EventSchema> }): EventSchema[];
2385
2684
  /** 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. */
2386
2685
  export function applyLifecycleWave(events: EventSchema[], uid: string, opts: { dormantFromDay: number; dormantDays: number; valueMomentEvent: string; resurrectBurst?: number; dropAll?: boolean }): EventSchema[];
2387
- /** 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. */
2686
+ /** v1.6.0 — append an ordered path after the FIRST chronological anchor for a deterministic `share` of users. Original traffic is untouched and can interrupt the immediate branch; share is an injection gate, not a measured Flows share. */
2388
2687
  export function applyPathBias(events: EventSchema[], uid: string, opts: { anchor: string; path: string[]; share: number; gapSeconds?: [number, number] }): EventSchema[];
2389
- /** v1.6.0 rewrite the user's timestamps into deterministic session clusters (n/week, m events, bounded span) that survive query-time re-derivation. */
2390
- export function applySessionShape(events: EventSchema[], uid: string, opts: { sessionsPerWeek: number; eventsPerSession: number; sessionMinutes: number }): EventSchema[];
2688
+ /** Retiming only; preserves every record. Both bounds omitted retain legacy full-UTC-day placement, including nonthrowing overfull requests whose clusters may merge. Optional inclusive bounds accept ISO, unix seconds or milliseconds (hook meta directly); an omitted side uses its original UTC day edge. The helper cannot infer datasetEnd. Explicit-bound mode throws RangeError before mutation for invalid bounds or insufficient per-week 30-minute-session capacity. Pass known metadata bounds to prevent later engine clipping. */
2689
+ export function applySessionShape(events: EventSchema[], uid: string, opts: { sessionsPerWeek: number; eventsPerSession: number; sessionMinutes: number; datasetStart?: string | number; datasetEnd?: string | number }): EventSchema[];
2391
2690
  }
2392
2691
 
2393
2692
  declare module '@ak--47/dungeon-master/hook-patterns' {
@@ -2627,6 +2926,9 @@ declare module '@ak--47/dungeon-master/verify' {
2627
2926
  */
2628
2927
  conversionWindow?: { unit: 'sessions'; n: number };
2629
2928
  graceperiod?: boolean;
2929
+ /** Defaults to false, including totals. Restart after inclusive 2s completion grace;
2930
+ * ordered shared last/first edges restart on the completion event itself.
2931
+ * graceperiod=false disables the wait; window expiry can restart earlier. */
2630
2932
  reentry?: boolean;
2631
2933
  exclusionSteps?: ExclusionStep[];
2632
2934
  trackStepProperties?: boolean | string[];
@@ -2659,9 +2961,10 @@ declare module '@ak--47/dungeon-master/verify' {
2659
2961
  }
2660
2962
  /** Evaluate a funnel against a user's events. Returns FunnelResult or array (totals mode). */
2661
2963
  export function evaluateFunnel(events: Array<Record<string, unknown>>, steps: FunnelStep[], options?: FunnelOptions): FunnelResult | FunnelResult[];
2662
- /** Hold Property Constant — runs parallel sub-funnels per unique value of `holdProperty`. */
2964
+ /** Hold Property Constant: parallel sub-funnels per held value; session ordinals use the full user stream. */
2663
2965
  export function evaluateFunnelHPC(events: Array<Record<string, unknown>>, steps: FunnelStep[], holdProperty: string, options?: FunnelOptions): Map<string | number, FunnelResult | FunnelResult[]>;
2664
- /** Pick a property snapshot from a FunnelResult for the given segment mode. */
2966
+ /** Merge reached snapshots in path order for first/last touch. Undefined and null preserve
2967
+ * defined non-null fallback values. Explicit step selection returns its snapshot unchanged. */
2665
2968
  export function resolveFunnelSegment(result: FunnelResult, mode: 'first' | 'last' | { step: number }): Record<string, unknown> | undefined;
2666
2969
  /** Normalize a FunnelStep to the `{ event, where? }` canonical shape. */
2667
2970
  export function normalizeStep(step: FunnelStep): { event: string; where?: { prop: string; op: string; value: unknown } };