@ak--47/dungeon-master 1.6.5 → 1.8.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 (45) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -11
  2. package/.claude/skills/create-dungeon/SKILL.md +49 -10
  3. package/.claude/skills/create-project/SKILL.md +22 -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 +18 -1
  7. package/.claude/skills/powertools/SKILL.md +20 -1
  8. package/.claude/skills/release-check/SKILL.md +99 -0
  9. package/.claude/skills/verify-dungeon/SKILL.md +71 -16
  10. package/.claude/skills/verify-dungeon/references/counting-semantics.md +14 -0
  11. package/.claude/skills/verify-dungeon/references/report-format.md +18 -1
  12. package/.claude/skills/verify-dungeon/references/sql-recipes.md +36 -1
  13. package/.claude/skills/warehouse-metrics/GAPS-template.md +34 -0
  14. package/.claude/skills/warehouse-metrics/SKILL.md +105 -0
  15. package/.claude/skills/warehouse-metrics/deploy.mjs +651 -0
  16. package/.claude/skills/write-hooks/SKILL.md +33 -3
  17. package/CHANGELOG.md +331 -0
  18. package/HOOKS.md +154 -5
  19. package/README.md +357 -8
  20. package/docs/guides/1.7.0-upgrade-guide.md +154 -0
  21. package/docs/guides/1.8.0-upgrade-guide.md +151 -0
  22. package/dungeons/technical/warehouse.js +187 -0
  23. package/index.js +131 -2
  24. package/lib/core/config-validator.js +264 -13
  25. package/lib/core/context.js +39 -0
  26. package/lib/core/dungeon-loader.js +5 -2
  27. package/lib/core/storage.js +51 -3
  28. package/lib/generators/events.js +53 -7
  29. package/lib/generators/funnels.js +85 -11
  30. package/lib/generators/profiles.js +9 -4
  31. package/lib/generators/standalone.js +248 -0
  32. package/lib/generators/warehouse.js +828 -0
  33. package/lib/orchestrators/mixpanel-sender.js +39 -3
  34. package/lib/orchestrators/user-loop.js +240 -9
  35. package/lib/templates/story-spec.schema.json +41 -16
  36. package/lib/utils/conditions.js +62 -0
  37. package/lib/utils/json-evaluator.js +12 -2
  38. package/lib/utils/utils.js +115 -19
  39. package/lib/verify/index.js +1 -0
  40. package/lib/verify/schema-validator.js +8 -0
  41. package/lib/verify/story-runner.js +71 -8
  42. package/lib/verify/warehouse.js +683 -0
  43. package/package.json +5 -11
  44. package/scripts/verify-stories.mjs +150 -44
  45. package/types.d.ts +606 -38
package/types.d.ts CHANGED
@@ -7,10 +7,60 @@
7
7
  type Primitives = string | number | boolean | Date | Record<string, any>;
8
8
 
9
9
  /**
10
- * A "validValue" can be a primitive, an array of valid values, or a thunk that
11
- * returns one. Configs use this everywhere properties are user-defined.
10
+ * v1.7.0 (P1-1) context handed to every property value function. Every member
11
+ * is optional: group profiles, lookup tables, ad spend and mirror props have no
12
+ * user, and `event` / `time` exist only while an event is being built.
13
+ *
14
+ * Existing zero-arity value functions keep working untouched — JavaScript ignores
15
+ * the extra argument. A function that DECLARES a parameter (`(ctx) => …`) is
16
+ * treated as context-aware and is never served from the source-string cache, so
17
+ * its result may legitimately differ per user or per event.
18
+ *
19
+ * @example
20
+ * userProps: {
21
+ * plan: ['free', 'pro'],
22
+ * revenue: (ctx) => ctx.profile.plan === 'pro' ? 100 : 10, // profile keys resolve in declaration order
23
+ * },
24
+ * superProps: {
25
+ * plan_on_event: (ctx) => ctx.profile.plan, // or use `stickyEventProps: ['plan']`
26
+ * total: (ctx) => ctx.event.price * ctx.event.quantity, // event props resolve in declaration order
27
+ * }
28
+ */
29
+ export interface ValueContext {
30
+ /** The user's resolved profile (partially built while userProps resolve). Undefined for group/lookup/ad-spend/mirror values. */
31
+ profile?: Record<string, any>;
32
+ /** The partially-built event record (identity + time set; earlier properties already resolved). Undefined outside event generation. */
33
+ event?: Record<string, any>;
34
+ /** The event's timestamp in unix milliseconds, when an event is being built. */
35
+ time?: number;
36
+ /** The validated dungeon config. */
37
+ config: Dungeon;
38
+ }
39
+
40
+ /**
41
+ * v1.7.0 (P2-1) — declarative weighted value. The numbers ARE the distribution:
42
+ * `{ __weights: { free: 60, pro: 30, enterprise: 10 } }` draws `free` 60% of the
43
+ * time. No automatic power law, no per-run winner; zero-weight keys never draw.
44
+ * Keys are strings (object keys), so numeric values come back as strings.
45
+ */
46
+ export interface WeightedValue {
47
+ __weights: Record<string, number>;
48
+ }
49
+
50
+ /**
51
+ * A "validValue" can be a primitive, an array of valid values, a declarative
52
+ * weighted form, or a function that returns one. Configs use this everywhere
53
+ * properties are user-defined.
54
+ *
55
+ * **Arrays of 3–19 unique strings get an automatic power-law draw** (~45% / 25% /
56
+ * 15% / decaying tail, one stable winner per array per run). Repeated entries are
57
+ * the weights (`['card', 'card', 'apple_pay']` = 2:1) and skip the power law.
58
+ * Opt out globally with `autoPowerLaw: false`, or state the distribution with
59
+ * `{ __weights }`.
60
+ *
61
+ * v1.7.0: the function arm receives a `ValueContext`. Zero-arity functions still work.
12
62
  */
13
- export type ValueValid = Primitives | ValueValid[] | (() => ValueValid);
63
+ export type ValueValid = Primitives | ValueValid[] | WeightedValue | ((ctx?: ValueContext) => ValueValid);
14
64
 
15
65
  /**
16
66
  * Mixpanel data residency region. Matches the set `mixpanel-import` accepts.
@@ -76,6 +126,12 @@ export interface DungeonSwitches {
76
126
  hasBrowser?: boolean;
77
127
  isAnonymous?: boolean;
78
128
  alsoInferFunnels?: boolean;
129
+ /** v1.7.0 — see `Dungeon.singleCountry`. */
130
+ singleCountry?: string;
131
+ /** v1.7.0 — see `Dungeon.campaignPerUser`. */
132
+ campaignPerUser?: boolean;
133
+ /** v1.7.0 — see `Dungeon.stickyEventProps`. */
134
+ stickyEventProps?: string[];
79
135
  }
80
136
 
81
137
  /**
@@ -309,10 +365,63 @@ export interface Dungeon {
309
365
  sessionTimeout?: number;
310
366
  /** If true, auto-generates funnels from the events array in addition to any explicit funnels. */
311
367
  alsoInferFunnels?: boolean;
312
- /** Restrict all location data to a single country (e.g., "US", "GB"). */
368
+ /**
369
+ * Restrict all location data (`hasLocation`) to one country. Accepts the ISO
370
+ * code (`"US"`, `"GB"`) or the full name (`"United States"`), case-insensitive.
371
+ *
372
+ * v1.7.0: a value that matches no country in the location template THROWS at
373
+ * validation. Before 1.7.0 a miss (including `"US"`, which only matched by full
374
+ * name) silently emptied the location pool and deleted every geo property from
375
+ * events and profiles. Also accepted inside `switches`.
376
+ */
313
377
  singleCountry?: string;
314
- /** If true, stops generation at exactly numEvents (forces concurrency=1). Without this, event count is approximate. */
378
+ /**
379
+ * If true, the run delivers exactly `numEvents` events (forces `concurrency: 1`).
380
+ *
381
+ * v1.7.0: exact. Per-user budgets are scaled during the run so the total lands on
382
+ * the target, and the last user's stream is trimmed with a seeded uniform sample
383
+ * so the count never exceeds `numEvents`. If the users' capacity (rate × active
384
+ * days, minus drops) cannot reach the target, the run stops short and
385
+ * `result.warnings` carries a `numEvents` entry with `requested` / `applied`.
386
+ * Before 1.7.0 the flag stopped on the generated count (drops included) and
387
+ * never topped up, landing ~6% short with 4x headroom available.
388
+ *
389
+ * Without this flag the count is approximate (rate × users × days, thinned by
390
+ * born-late users and drops).
391
+ */
315
392
  strictEventCount?: boolean;
393
+ /**
394
+ * v1.7.0 (P1-2) — profile properties projected onto every event of the user.
395
+ * Each key must be declared in `userProps`, a persona's `properties`, or
396
+ * `superProps` (schema-first; the validator throws otherwise). Keys from the
397
+ * profile copy the profile's value (after the `user` hook). Keys declared only
398
+ * in `superProps` are resolved ONCE per user and held constant instead of
399
+ * re-rolled per event. Sticky values land after `superProps` and before the
400
+ * `event` hook, so hooks remain the final authority.
401
+ *
402
+ * The declarative alternative to `superProps: { plan: (ctx) => ctx.profile.plan }`.
403
+ * Also accepted inside `switches`.
404
+ */
405
+ stickyEventProps?: string[];
406
+ /**
407
+ * v1.7.0 (P1-4) — one acquisition campaign per user. With `hasCampaigns: true`,
408
+ * each user draws one campaign template at birth; its `utm_source`,
409
+ * `utm_campaign`, `utm_medium`, `utm_content`, `utm_term` are stamped on the
410
+ * profile and every sampled touchpoint carries those same values instead of a
411
+ * fresh random template per event. Any UTM key already on the profile (from a
412
+ * persona's `properties` or the `user` hook) wins over the draw, so
413
+ * `personas: [{ name: 'paid', properties: { utm_source: 'google', utm_medium: 'cpc' } }]`
414
+ * makes "paid search converts better" declarative. Default `false`. Also
415
+ * accepted inside `switches`.
416
+ */
417
+ campaignPerUser?: boolean;
418
+ /**
419
+ * v1.7.0 (P2-1) — set `false` to turn off the automatic power-law draw on
420
+ * arrays of 3–19 unique strings for the whole run (uniform picks instead).
421
+ * Default `true` (pre-1.7 behavior). Prefer `{ __weights }` when you want a
422
+ * specific distribution.
423
+ */
424
+ autoPowerLaw?: boolean;
316
425
  /** Internal flag for UI-triggered jobs (affects SCD credential handling). */
317
426
  isUIJob?: boolean;
318
427
 
@@ -344,6 +453,18 @@ export interface Dungeon {
344
453
  groupProps?: Record<string, Record<string, ValueValid>>;
345
454
  /** Lookup table definitions for dimension tables. */
346
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[];
347
468
  /** TimeSoup configuration: shapes intra-week and intra-day rhythm (peaks, deviation, DOW/HOD weights). Pair with `macro` for big-picture trend control. */
348
469
  soup?: soup;
349
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. */
@@ -441,6 +562,16 @@ export interface Dungeon {
441
562
  *
442
563
  * Example: `{ day1: 0.40, day7: 0.20, day30: 0.08 }` produces a curve that
443
564
  * approximates a typical product's 30-day retention.
565
+ *
566
+ * **Day 1 has a floor the curve cannot move (v1.7.0 doc, P1-6).** The curve
567
+ * picks which UTC days a user gets a SESSION; retention counts EVENTS. A funnel
568
+ * opened on the birth day spills its later steps across the following
569
+ * `timeToConvert` hours regardless of the day plan, so day-1 retention sits
570
+ * near 0.85 for funnel-driven dungeons no matter what `day1` says (measured
571
+ * 0.885 for an asked 0.15; days 7 and 30 followed the curve). Treat `day1` as
572
+ * governed by session and funnel structure and verify the curve from day 7 on.
573
+ * To lower day 1, shorten `timeToConvert` on funnels users enter on birth, or
574
+ * use an `everything` hook to drop next-day spill.
444
575
  */
445
576
  retentionCurve?: {
446
577
  type?: 'logarithmic' | 'linear';
@@ -532,13 +663,27 @@ export type MacroPreset = "flat" | "steady" | "growth" | "viral" | "decline";
532
663
 
533
664
  /**
534
665
  * Macro configuration object — fine-grained big-picture trend control.
666
+ *
667
+ * **Canonical spelling:** `macro: { preset, ...overrides }`. The top-level
668
+ * `bornRecentBias` / `percentUsersBornInDataset` / `preExistingSpread` keys are
669
+ * a legacy alias and win over the object when both are set.
670
+ *
671
+ * **Two shapes, two contracts (v1.7.0, R2-1):**
672
+ * - `{ preset: 'growth', percentUsersBornInDataset: 50 }` — a NAMED preset is a
673
+ * shape contract, so its born% cap applies (flat 12, steady 12, growth 30,
674
+ * viral 55, decline 5). Values above the cap are clamped and reported in
675
+ * `result.warnings` (`key: 'percentUsersBornInDataset'`).
676
+ * - `{ bornRecentBias: 0.3, percentUsersBornInDataset: 50 }` — NO `preset` is a
677
+ * custom macro: you own the shape, no cap applies, overrides are honored as
678
+ * written (missing fields fill from `flat`). Before 1.7.0 this shape was
679
+ * silently capped at 12.
535
680
  */
536
681
  export type MacroConfig = {
537
- /** Use a named macro preset as the base, then override individual fields. */
682
+ /** Use a named macro preset as the base, then override individual fields. Omit for a custom, uncapped macro. */
538
683
  preset?: MacroPreset;
539
- /** Bias for birth dates. -1..1; negative = early skew, positive = recent skew, 0 = uniform. */
684
+ /** Bias for birth dates. -1..1; negative = early skew, positive = recent skew, 0 = uniform. User-explicit values are clamped to [-0.5, 0.5]. */
540
685
  bornRecentBias?: number;
541
- /** Percentage of users born in dataset window (0..100). */
686
+ /** Percentage of users born in dataset window (0..100). Capped per named preset; uncapped without `preset`. */
542
687
  percentUsersBornInDataset?: number;
543
688
  /** "pinned" = pre-existing users stack at FIXED_BEGIN; "uniform" = spread across [FIXED_BEGIN-30d, FIXED_BEGIN]. */
544
689
  preExistingSpread?: "pinned" | "uniform";
@@ -570,7 +715,7 @@ export interface ResolvedMacro {
570
715
  * - "everything" — array of ALL events for one user (return array to replace; meta.profile available)
571
716
  *
572
717
  * Storage-only hooks (fire during hookPush, not in generators):
573
- * - "ad-spend", "group", "mirror", "lookup"
718
+ * - "ad-spend", "group", "mirror", "lookup", "standalone", "warehouse"
574
719
  */
575
720
  export type hookTypes =
576
721
  | "event"
@@ -583,6 +728,8 @@ export type hookTypes =
583
728
  | "funnel-pre"
584
729
  | "funnel-post"
585
730
  | "ad-spend"
731
+ | "standalone"
732
+ | "warehouse"
586
733
  | "churn"
587
734
  | "group-event"
588
735
  | "everything"
@@ -599,7 +746,9 @@ export type hookTypes =
599
746
  * - "event": return value REPLACES the event (must be the event object).
600
747
  * - "everything": return an array to REPLACE the user's event list (filter/inject/dedupe).
601
748
  * - "user", "scd-pre", "funnel-pre", "funnel-post": return value is IGNORED — mutate in place.
602
- * - 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".
603
752
  *
604
753
  * @param record - The data being processed (event, profile, array of events, funnel config, etc.).
605
754
  * @param type - Which hook type is firing — see `hookTypes`.
@@ -775,6 +924,10 @@ export interface hookArrayOptions<T> {
775
924
  concurrency?: number;
776
925
  /** Generation context (config, runtime, defaults). */
777
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[];
778
931
  }
779
932
 
780
933
  /**
@@ -796,6 +949,10 @@ export interface HookedArray<T> extends Array<T> {
796
949
  getWritePath: () => string;
797
950
  /** Returns all file paths written by this container during the current run. */
798
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;
799
956
  /** SCD prop name this array carries (only set on SCD HookedArrays). */
800
957
  scdKey?: string;
801
958
  /** Entity type for SCDs ("user" or a group key). */
@@ -804,6 +961,10 @@ export interface HookedArray<T> extends Array<T> {
804
961
  groupKey?: string;
805
962
  /** Lookup table key this array carries (only set on lookup table HookedArrays). */
806
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[];
807
968
  }
808
969
 
809
970
  export type AllData =
@@ -821,8 +982,11 @@ export interface Storage {
821
982
  mirrorEventData?: HookedArray<EventSchema>;
822
983
  userProfilesData?: HookedArray<UserProfile>;
823
984
  adSpendData?: HookedArray<EventSchema>;
985
+ standaloneEventData?: HookedArray<EventSchema>;
824
986
  groupProfilesData?: HookedArray<GroupProfileSchema>[];
825
987
  lookupTableData?: HookedArray<LookupTableSchema>[];
988
+ warehouseMetricData?: HookedArray<Record<string, any>>[];
989
+ warehouseManifestFile?: string;
826
990
  scdTableData?: HookedArray<SCDSchema>[];
827
991
  groupEventData?: HookedArray<EventSchema>;
828
992
  }
@@ -835,6 +999,10 @@ export interface RuntimeState {
835
999
  eventCount: number;
836
1000
  storedEventCount: number;
837
1001
  userCount: number;
1002
+ /** v1.7.0 (R2-5): profiles pushed to storage (ticks at push time; batch-mode safe). */
1003
+ profilesGenerated: number;
1004
+ /** v1.7.0 (R2-5): `_drop`-flagged profiles pushed to storage (never sent to /engage). */
1005
+ profilesDropped: number;
838
1006
  isBatchMode: boolean;
839
1007
  verbose: boolean;
840
1008
  }
@@ -886,6 +1054,14 @@ export interface Context {
886
1054
  FIXED_NOW: number;
887
1055
  /** Start of the resolved dataset window (unix seconds). Equal to the user-supplied `datasetStart`, or fallback `today_start - numDays`. */
888
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;
889
1065
  /** Alias of `FIXED_BEGIN` — surfaced on hook `meta.datasetStart`. */
890
1066
  DATASET_START_SECONDS: number;
891
1067
  /** Alias of `FIXED_NOW` — surfaced on hook `meta.datasetEnd`. */
@@ -897,6 +1073,14 @@ export interface Context {
897
1073
  incrementUsers(): void;
898
1074
  incrementStoredEvents(count?: number): void;
899
1075
  setStorage(storage: Storage): void;
1076
+ /** v1.7.0 (P2-2): record or bump an aggregated runtime warning (keyed by `entry.key`). */
1077
+ addWarning(entry: EngineWarning): void;
1078
+ /** v1.7.0 (P2-2): aggregated runtime warnings collected so far. */
1079
+ getWarnings(): EngineWarning[];
1080
+ /** v1.7.0 (R2-5) */
1081
+ incrementProfilesGenerated(): void;
1082
+ /** v1.7.0 (R2-5) */
1083
+ incrementProfilesDropped(): void;
900
1084
 
901
1085
  // State getter methods
902
1086
  getOperations(): number;
@@ -926,7 +1110,15 @@ export interface EventConfig {
926
1110
  properties?: Record<string, ValueValid>;
927
1111
  /** If true, this is the user's first-ever event (e.g., "sign up"). Used to create onboarding funnels. */
928
1112
  isFirstEvent?: boolean;
929
- /** If true, generating this event signals the user has churned. The user stops producing further events unless returnLikelihood allows them to come back. */
1113
+ /**
1114
+ * If true, generating this event signals the user has churned. The user stops
1115
+ * producing further events unless returnLikelihood allows them to come back.
1116
+ *
1117
+ * A churn event in the standalone pool is drawn by weight like any other, so it
1118
+ * ends every user after roughly `total weight / its weight` events — which caps
1119
+ * per-user volume and washes out `personas[].eventMultiplier` (measured 1.04x
1120
+ * for an asked 3x). Keep its weight low, or drive churn from a hook.
1121
+ */
930
1122
  isChurnEvent?: boolean;
931
1123
  /** Probability (0-1) that a churned user returns and continues generating events. 0 = permanent churn, 1 = always returns. Only used when isChurnEvent is true. Default: 0 */
932
1124
  returnLikelihood?: number;
@@ -1070,10 +1262,33 @@ export interface Funnel {
1070
1262
  */
1071
1263
  props?: Record<string, ValueValid>;
1072
1264
  /**
1073
- * funnel conditions (user properties) are used to filter users who are eligible for the funnel
1074
- * these conditions must match the current user's profile for the user to be eligible for the funnel
1265
+ * Profile conditions a user must satisfy to be offered this funnel (AND across
1266
+ * keys). The only engine mechanism that makes ONE segment convert differently on
1267
+ * ONE funnel — `personas[].conversionModifier` applies to every funnel.
1268
+ *
1269
+ * Each value is a scalar (strict equality) or an operator map:
1270
+ * `eq`, `neq`, `in`, `nin`, `gt`, `gte`, `lt`, `lte` (v1.7.0). Operators within
1271
+ * one key AND together. No `or`.
1272
+ *
1273
+ * The duplicate-funnel idiom: two funnels with the same `name` and `sequence`,
1274
+ * different `conditions` and rates.
1275
+ *
1276
+ * @example
1277
+ * userProps: { platform: ['iOS', 'Android'], seats: [1, 5, 10, 20] },
1278
+ * funnels: [
1279
+ * { name: 'Checkout', sequence: [...], conditions: { platform: 'iOS' }, conversionRate: 80 },
1280
+ * { name: 'Checkout', sequence: [...], conditions: { platform: 'Android' }, conversionRate: 40 },
1281
+ * { name: 'Upgrade', sequence: [...], conditions: { seats: { gte: 10 }, platform: { in: ['iOS', 'Android'] } } },
1282
+ * ]
1283
+ *
1284
+ * Validation (v1.7.0): function values, bare arrays, unknown operators, and
1285
+ * `in`/`nin` without an array THROW — those shapes silently never matched before.
1286
+ * A key not declared in `userProps` / `superProps` / any persona's `properties`
1287
+ * produces a `result.warnings` entry (only a `user` hook could supply it). Users
1288
+ * who match no funnel at all fall through to standalone events; the run reports
1289
+ * how many under `key: 'funnels.conditions'`.
1075
1290
  */
1076
- conditions?: Record<string, ValueValid>;
1291
+ conditions?: FunnelConditions;
1077
1292
  /**
1078
1293
  * Experiment configuration for this funnel.
1079
1294
  *
@@ -1131,7 +1346,9 @@ export interface Funnel {
1131
1346
  }>;
1132
1347
 
1133
1348
  /** @internal Resolved experiment config set by config-validator. */
1134
- _experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null; sticky: boolean };
1349
+ _experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null; sticky: boolean; stampProfile: boolean };
1350
+ /** @internal v1.7.0 — set by the validator on the engine-synthesized catch-all funnel. */
1351
+ _catchAll?: boolean;
1135
1352
  /** @internal Set by funnels.js during experiment handling. */
1136
1353
  _experimentName?: string;
1137
1354
  /** @internal Set by funnels.js during experiment handling. */
@@ -1214,8 +1431,43 @@ export interface ExperimentConfig {
1214
1431
  * variants across passes.
1215
1432
  */
1216
1433
  sticky?: boolean;
1434
+ /**
1435
+ * v1.7.0 (P0-2) — write the assigned variant onto the user profile as
1436
+ * `"Experiment: <name>": "<variant>"` so a funnel can be broken down by variant
1437
+ * without building a cohort from the exposure event. Default `true`. Stamped
1438
+ * lazily when the user is first exposed (respects `startDaysBeforeEnd`), so
1439
+ * never-exposed users carry no property and the `user` hook does not see it;
1440
+ * the `everything` hook does. Ignored when `sticky: false` (a re-rolled variant
1441
+ * has no single per-user value). The variant is NOT stamped on downstream funnel
1442
+ * step events (that would add undeclared columns).
1443
+ */
1444
+ stampProfile?: boolean;
1445
+ }
1446
+
1447
+ /**
1448
+ * v1.7.0 (P0-1) — one funnel condition: a scalar (strict equality) or an operator map.
1449
+ */
1450
+ export type FunnelCondition = Primitives | FunnelConditionOperators;
1451
+
1452
+ /** Operator map for a funnel condition. Every operator present must hold (AND). */
1453
+ export interface FunnelConditionOperators {
1454
+ /** strict equality */
1455
+ eq?: Primitives;
1456
+ /** strict inequality (a missing profile key satisfies it) */
1457
+ neq?: Primitives;
1458
+ /** value is one of these */
1459
+ in?: Primitives[];
1460
+ /** value is none of these (a missing profile key satisfies it) */
1461
+ nin?: Primitives[];
1462
+ gt?: number | string;
1463
+ gte?: number | string;
1464
+ lt?: number | string;
1465
+ lte?: number | string;
1217
1466
  }
1218
1467
 
1468
+ /** Map of profile key → condition. AND across keys. */
1469
+ export type FunnelConditions = Record<string, FunnelCondition>;
1470
+
1219
1471
  /** A single variant in an experiment. */
1220
1472
  export interface ExperimentVariant {
1221
1473
  /** Display name — appears in the "Variant name" property on $experiment_started. */
@@ -1328,11 +1580,47 @@ export interface GroupProfileSchema {
1328
1580
  */
1329
1581
  export interface ImportResults {
1330
1582
  events: ImportResult;
1331
- users: ImportResult;
1583
+ /**
1584
+ * v1.7.0 (R2-5): mixpanel-import's receipt plus two engine counters, so the
1585
+ * consumer can reconcile without guessing:
1586
+ * `generated - dropped_anonymous - failed === success`.
1587
+ */
1588
+ users: ImportResult & {
1589
+ /** Profiles the engine pushed to storage (bots included). */
1590
+ generated: number;
1591
+ /** `_drop`-flagged anonymous non-converters that were never sent to /engage. */
1592
+ dropped_anonymous: number;
1593
+ };
1332
1594
  groups: ImportResult[];
1333
1595
  }
1334
1596
  type ImportResult = import("mixpanel-import").ImportResults;
1335
1597
 
1598
+ /**
1599
+ * v1.7.0 (P2-2) — one value the engine changed or flagged. `result.warnings` is
1600
+ * always present (empty array when nothing was touched), regardless of `verbose`.
1601
+ *
1602
+ * Validator clamps come first (`severity: 'clamp'`, `applied !== requested`),
1603
+ * then run-level aggregates: conversionRate saturation per funnel and source
1604
+ * (`key: 'funnels[<name>].conversionRate:<source>'`), users matching no
1605
+ * conditioned funnel (`'funnels.conditions'`), churn washing out a persona
1606
+ * multiplier (`'personas.eventMultiplier'`), and a strictEventCount shortfall
1607
+ * (`'numEvents'`). Aggregated entries carry `count`.
1608
+ */
1609
+ export interface EngineWarning {
1610
+ /** Config path the entry is about, e.g. `percentUsersBornInDataset`, `funnels[Checkout].conversionRate:persona "power" conversionModifier`. */
1611
+ key: string;
1612
+ /** What the config (or a modifier) asked for. Undefined when nothing was requested (auto-set). */
1613
+ requested?: unknown;
1614
+ /** What the engine used. Equals `requested` for pure warnings. */
1615
+ applied?: unknown;
1616
+ /** Plain-language explanation and what to change. */
1617
+ reason: string;
1618
+ /** `clamp` = a value was changed; `warn` = flagged, nothing changed. */
1619
+ severity: 'clamp' | 'warn';
1620
+ /** Number of occurrences folded into this entry (runtime aggregates only). */
1621
+ count?: number;
1622
+ }
1623
+
1336
1624
  /**
1337
1625
  * the end result of the data generation
1338
1626
  */
@@ -1347,12 +1635,23 @@ export type Result = {
1347
1635
  scdTableData: SCDSchema[][];
1348
1636
  /** Ad-spend events (only populated when `hasAdSpend: true`). */
1349
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;
1350
1644
  /** Group profiles — one inner array per group key. */
1351
1645
  groupProfilesData: GroupProfileSchema[][];
1352
1646
  /** Lookup tables — one inner array per table. */
1353
1647
  lookupTableData: LookupTableData[][];
1354
1648
  /** Mixpanel import results (only populated when a token was provided). */
1355
1649
  importResults?: ImportResults;
1650
+ /**
1651
+ * v1.7.0 (P2-2): every value the engine clamped or flagged this run. Always
1652
+ * present, even when empty. See `EngineWarning`.
1653
+ */
1654
+ warnings: EngineWarning[];
1356
1655
  /** Absolute paths of all files written to disk. */
1357
1656
  files?: string[];
1358
1657
  /** Timing information. */
@@ -1454,34 +1753,38 @@ export interface Persona {
1454
1753
  name: string;
1455
1754
  /** Relative weight for persona assignment (higher = more users get this persona). */
1456
1755
  weight: number;
1457
- /** Multiplier for number of events this persona generates (1.0 = normal). */
1756
+ /**
1757
+ * Multiplier on the persona's whole per-user event budget (1.0 = normal). The
1758
+ * budget drives BOTH usage-funnel passes and standalone events, so a 3x persona
1759
+ * runs ~3x as many funnel passes (measured 2.9–3.2x).
1760
+ *
1761
+ * **`isChurnEvent` caps it.** A churn event in the standalone pool ends each
1762
+ * user after roughly the same number of events regardless of budget, so the
1763
+ * multiplier washes out (measured 1.04x for an asked 3x with a weight-1 churn
1764
+ * event among 16 weight units). When more than half the users churn and a
1765
+ * persona multiplier is in play, `result.warnings` carries
1766
+ * `key: 'personas.eventMultiplier'`. Lower the churn event's weight, raise
1767
+ * `returnLikelihood`, or drive churn from a hook.
1768
+ */
1458
1769
  eventMultiplier?: number;
1459
- /** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). */
1770
+ /** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). Applies to every funnel; for one segment on one funnel use `Funnel.conditions`. */
1460
1771
  conversionModifier?: number;
1461
1772
  /**
1462
- * Base churn rate for this persona (0-1).
1463
- * @deprecated unimplemented; no-op. The validator defaults it to 0 but no
1464
- * generator reads it. Model churn with hooks instead (HOOKS.md §3 cohort
1465
- * atoms + `engagementDecay`).
1773
+ * v1.7.0 (P1-3) multiplier for funnel `timeToConvert` (1.0 = normal,
1774
+ * 0.5 = converts twice as fast). Composes after an experiment's `ttcMultiplier`
1775
+ * and before the `funnel-pre` hook. Must be a positive number.
1466
1776
  */
1467
- churnRate?: number;
1777
+ ttcModifier?: number;
1468
1778
  /** Properties merged into user profiles for this persona. */
1469
1779
  properties?: Record<string, ValueValid>;
1470
1780
  /**
1471
- * Limit how long this persona is active (e.g., trial users active for 14 days).
1472
- * @deprecated — unimplemented; no-op. Declared surface only; nothing in lib/
1473
- * reads it. Bound activity windows with an `everything` hook (drop events
1474
- * outside the window) instead.
1781
+ * Per-persona engagement decay override. This one IS implemented
1782
+ * (`user-loop.js` reads `persona.engagementDecay` before the global one).
1783
+ *
1784
+ * v1.7.0 removed the never-implemented `churnRate`, `activeWindow`, and
1785
+ * `soupOverride` from this type. The validator still accepts and warns on them.
1475
1786
  */
1476
- activeWindow?: { maxDays: number };
1477
- /** Per-persona engagement decay override. */
1478
1787
  engagementDecay?: EngagementDecay;
1479
- /**
1480
- * Per-persona soup/timing override.
1481
- * @deprecated — unimplemented; no-op. Declared surface only; nothing in lib/
1482
- * reads it. Use the top-level `soup` config for timing shape.
1483
- */
1484
- soupOverride?: SoupConfig;
1485
1788
  }
1486
1789
 
1487
1790
  /**
@@ -1496,7 +1799,18 @@ export interface WorldEvent {
1496
1799
  startDay: number;
1497
1800
  /** Duration in days (0.25 = 6 hours, null = permanent from startDay onward). */
1498
1801
  duration?: number | null;
1499
- /** Volume multiplier during this event (3.0 = 3x events, 0.1 = 90% drop). */
1802
+ /**
1803
+ * Volume multiplier during this event (3.0 = 3x events, 0.1 = 90% drop).
1804
+ *
1805
+ * Below 1: affected events are dropped at random so volume falls to the multiple.
1806
+ * Above 1 (v1.7.0, P0-3): affected in-window events are CLONED — `floor(m - 1)`
1807
+ * copies plus one more with probability `frac(m)` (2.5 = one guaranteed clone
1808
+ * and a 50% second) — each with a fresh `insert_id` and a timestamp spread
1809
+ * uniformly across the window (never past the dataset end). Measured 3.06x for
1810
+ * an asked 3x; before 1.7.0 values above 1 were a silent no-op (measured 1.08x).
1811
+ * Clones exist before `engagementDecay` and every hook, so they are visible to
1812
+ * `everything`. The same rule applies to `aftermath.volumeMultiplier`.
1813
+ */
1500
1814
  volumeMultiplier?: number;
1501
1815
  /** Conversion rate modifier during this event. */
1502
1816
  conversionModifier?: number;
@@ -1738,9 +2052,10 @@ export interface StoryAssertion {
1738
2052
  /**
1739
2053
  * Byte-compatible with `emulateBreakdown` / `verifyDungeon` args — or the
1740
2054
  * `{ type: 'duckdb', sql }` escape hatch (disk mode only; `{{PREFIX}}` in
1741
- * 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 }`.
1742
2057
  */
1743
- breakdown: Record<string, unknown> & { type: string; sql?: string };
2058
+ breakdown: Record<string, unknown> & { type: string; sql?: string; table?: string };
1744
2059
  select?: StorySelect;
1745
2060
  expect?: StoryExpect;
1746
2061
  /**
@@ -2056,13 +2371,266 @@ export interface WritePaths {
2056
2371
  eventFiles: string[];
2057
2372
  userFiles: string[];
2058
2373
  adSpendFiles: string[];
2374
+ standaloneFiles: string[];
2059
2375
  scdFiles: string[];
2060
2376
  mirrorFiles: string[];
2061
2377
  groupFiles: string[];
2062
2378
  lookupFiles: string[];
2379
+ warehouseFiles: string[];
2063
2380
  folder: string;
2064
2381
  }
2065
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
+
2066
2634
  /**
2067
2635
  * Configuration for TimeSoup time distribution function
2068
2636
  */