@revturbine/cli 0.19.4 → 0.19.5

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 (2) hide show
  1. package/dist/cli.js +1122 -43
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2354,6 +2354,7 @@ var CtaActionTypeSchema = z2.enum([
2354
2354
  "custom"
2355
2355
  ]).meta({ id: "CtaActionType", "x-revturbine-schema-persistence": Transient, "x-revturbine-schema-exposure": External });
2356
2356
  var ObjectiveField = HandleField.optional();
2357
+ var NullableObjectiveField = z2.string().min(1).max(100).nullable().optional();
2357
2358
  var SchemaContext = {
2358
2359
  Playbook: "playbook",
2359
2360
  Branding: "branding",
@@ -3001,10 +3002,14 @@ var EntitlementRuleSchema = IdField.merge(TimestampFields).merge(TenantIdField).
3001
3002
  visibility: RuleVisibilitySchema.default("public").meta(Unrestricted3),
3002
3003
  // Business objective this rule monetizes for — the analytics `objective`
3003
3004
  // slice, which resolves `rule_handle → objective` through a configuration
3004
- // snapshot (BL-0065 / worksheet G4). Config-only, and deliberately NOT part
3005
- // of the minted identity below: an objective relabel is a behaviour-only
3006
- // edit that coalesces onto the same rule, not a new rule scope.
3007
- objective: ObjectiveField.meta(Unrestricted3),
3005
+ // snapshot (BL-0065 / worksheet G4). A reference by handle to an
3006
+ // `objectives[]` entity (D-15; VAL-OBJ-01). Config-only, and deliberately
3007
+ // NOT part of the minted identity below: an objective relabel is a
3008
+ // behaviour-only edit that coalesces onto the same rule, not a new rule scope.
3009
+ // Nullable on the persisted entity so an editor can CLEAR the reference
3010
+ // (a PATCH merges, so omission keeps the old value); the portable
3011
+ // projection stays optional-only and omits it when unset.
3012
+ objective: NullableObjectiveField.meta(Unrestricted3),
3008
3013
  // Usage-Limit "measured over" window, rule-level (plan #55). Rate Limit
3009
3014
  // keeps its entitlement-level `period_scope`; this is the per-rule one.
3010
3015
  period_scope: UsagePeriodScopeSchema.optional().meta(Unrestricted3),
@@ -3360,11 +3365,15 @@ var PlacementSchema = IdField.merge(TimestampFields).merge(TenantIdField).merge(
3360
3365
  category: PlacementCategorySchema.meta(Unrestricted4),
3361
3366
  // Business objective this placement monetizes for — the analytics
3362
3367
  // `objective` slice, which resolves `placement_id → objective` through a
3363
- // configuration snapshot (BL-0065 / worksheet G4). Config-only: nothing in
3364
- // the runtime decision reads it, so it is absent from the portable
3365
- // RevTurbineConfig projection and the compiled payload. Free handle; see
3366
- // `ObjectiveField` for why the value set is not enumerated here.
3367
- objective: ObjectiveField.meta(Unrestricted4),
3368
+ // configuration snapshot (BL-0065 / worksheet G4). A reference by handle to
3369
+ // an `objectives[]` entity (D-15; VAL-OBJ-01). Config-only: nothing in the
3370
+ // runtime decision reads it, so it round-trips through the portable
3371
+ // Playbook projection but never lowers into the compiled IR. See
3372
+ // `ObjectiveField`.
3373
+ // Nullable on the persisted entity so an editor can CLEAR the reference
3374
+ // (a PATCH merges, so omission keeps the old value); the portable
3375
+ // projection stays optional-only and omits it when unset.
3376
+ objective: NullableObjectiveField.meta(Unrestricted4),
3368
3377
  drag_order_in_category: z7.number().int().default(0).meta(Unrestricted4),
3369
3378
  // Trigger config (populated based on category)
3370
3379
  surface_slot_id: z7.string().optional().meta(Unrestricted4),
@@ -3844,6 +3853,34 @@ var UserPlanContextSchema = z9.object({
3844
3853
  }).meta(
3845
3854
  { id: "UserPlanContext", "x-revturbine-schema-persistence": Transient6, "x-revturbine-schema-exposure": External5 }
3846
3855
  );
3856
+ var BuiltinActivityLevelSchema = z9.enum(["new", "high", "medium", "low", "inactive"]);
3857
+ var BuiltinSubscriptionStateSchema = z9.enum(["none", "trial", "paid", "cancelled"]);
3858
+ var BuiltinTrialTypeSchema = z9.enum(["none", "free_trial", "reverse_trial"]);
3859
+ var BuiltinBuyerRoleSchema = z9.enum(["buyer", "non_buyer"]);
3860
+ var BuiltinEmailTypeSchema = z9.enum(["business", "personal", "unknown"]);
3861
+ var BuiltinBillingHealthSchema = z9.enum([
3862
+ "no_billing",
3863
+ "good_standing",
3864
+ "trial_payment_method_attached",
3865
+ "payment_method_missing",
3866
+ "payment_failed",
3867
+ "payment_overdue",
3868
+ "cancelled"
3869
+ ]);
3870
+ var BuiltinRegionSchema = z9.enum(["us_canada", "europe", "rest_of_world"]);
3871
+ var BuiltinDeviceTypeSchema = z9.enum(["desktop", "mobile", "tablet", "unknown"]);
3872
+ var BuiltinSeatTypeSchema = z9.string().regex(/^[a-z0-9._]{1,87}$/);
3873
+ var UserBuiltinDimensionsSchema = z9.object({
3874
+ activity_level: BuiltinActivityLevelSchema.optional().meta(Unrestricted6),
3875
+ subscription_state: BuiltinSubscriptionStateSchema.optional().meta(Unrestricted6),
3876
+ trial_type: BuiltinTrialTypeSchema.optional().meta(Unrestricted6),
3877
+ seat_type: BuiltinSeatTypeSchema.optional().meta(Unrestricted6),
3878
+ buyer_role: BuiltinBuyerRoleSchema.optional().meta(Unrestricted6),
3879
+ email_type: BuiltinEmailTypeSchema.optional().meta(Unrestricted6),
3880
+ billing_health: BuiltinBillingHealthSchema.optional().meta(Unrestricted6),
3881
+ region: BuiltinRegionSchema.optional().meta(Unrestricted6),
3882
+ device_type: BuiltinDeviceTypeSchema.optional().meta(Unrestricted6)
3883
+ });
3847
3884
  var UserInstanceContextSchema = z9.object({
3848
3885
  product_instance_id: z9.string().min(1).meta(Unrestricted6),
3849
3886
  user_id: z9.string().min(1).meta(Pii2),
@@ -3906,13 +3943,27 @@ var UserContextSchema = IdField.merge(TenantIdField).merge(TimestampFields).exte
3906
3943
  /** Customer-defined fields: role, app-level permissions, display prefs. */
3907
3944
  custom: z9.record(z9.string(), z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])).default({}).meta(Pii2),
3908
3945
  /**
3909
- * Transient personalization token map.
3946
+ * Personalization token map.
3910
3947
  *
3911
3948
  * Holds SDK-derived tokens (plan_name, usage_current, etc.) merged with
3912
- * app-provided tokens. Not persisted to the backend — rebuilt on each
3913
- * SDK session from context + exported config.
3949
+ * app-provided tokens; the SDK rebuilds it on each session from context +
3950
+ * exported config.
3951
+ *
3952
+ * Deliberately NOT marked field-level Transient: the `user_contexts` table
3953
+ * carries a `personalization` jsonb column (web `drizzle/0000_*.sql`) and
3954
+ * the generic user-context write persists it (web
3955
+ * `src/lib/user-context/sanitize-write.ts` redacts it first). The drizzle
3956
+ * generator emits no column for a field-level Transient (plan 279
3957
+ * TASK-3), so the label has to match the table.
3914
3958
  */
3915
- personalization: z9.record(z9.string(), z9.union([z9.string(), z9.number()])).default({}).meta({ ...Unrestricted6, "x-revturbine-schema-persistence": Transient6 }),
3959
+ personalization: z9.record(z9.string(), z9.union([z9.string(), z9.number()])).default({}).meta(Unrestricted6),
3960
+ /**
3961
+ * Built-in segment dimension values (plan 279 PD-3) — see
3962
+ * {@link UserBuiltinDimensionsSchema}. Field-level Transient: no DB
3963
+ * column; assembled at retrieval, or set by the app in `local_only` mode
3964
+ * and on the server ports.
3965
+ */
3966
+ builtin_dimensions: UserBuiltinDimensionsSchema.optional().meta({ ...Unrestricted6, "x-revturbine-schema-persistence": Transient6 }),
3916
3967
  // ── Derived-entitlement cache (plan 74 REQ-12/REQ-13) ──────────────
3917
3968
  // `entitlements` above is the rule-evaluated projection — a CACHE, not
3918
3969
  // source of truth. These stamps record what it was computed against so a
@@ -3998,6 +4049,18 @@ var ClientContextPlanSchema = z9.object({
3998
4049
  }).meta(
3999
4050
  { id: "ClientContextPlan", "x-revturbine-schema-persistence": Transient6, "x-revturbine-schema-exposure": Internal5 }
4000
4051
  );
4052
+ var ClientContextBuiltinDimensionsSchema = z9.object({
4053
+ activity_level: BuiltinActivityLevelSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4054
+ subscription_state: BuiltinSubscriptionStateSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4055
+ trial_type: BuiltinTrialTypeSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4056
+ seat_type: BuiltinSeatTypeSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4057
+ buyer_role: BuiltinBuyerRoleSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4058
+ email_type: BuiltinEmailTypeSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4059
+ region: BuiltinRegionSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4060
+ device_type: BuiltinDeviceTypeSchema.optional().meta({ ...Unrestricted6, ...ClientSafe })
4061
+ }).meta(
4062
+ { id: "ClientContextBuiltinDimensions", "x-revturbine-schema-persistence": Transient6, "x-revturbine-schema-exposure": Internal5 }
4063
+ );
4001
4064
  var ClientContextSchema = z9.object({
4002
4065
  /** The end-user subject the client token was minted for (carried by the token, not the request). */
4003
4066
  subject: z9.string().min(1).meta({ ...Unrestricted6, ...ClientSafe }),
@@ -4006,7 +4069,9 @@ var ClientContextSchema = z9.object({
4006
4069
  trial: ClientContextTrialSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4007
4070
  billing: ClientContextBillingSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4008
4071
  plan: ClientContextPlanSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4009
- capabilities: ClientContextCapabilitiesSchema.optional().meta({ ...Unrestricted6, ...ClientSafe })
4072
+ capabilities: ClientContextCapabilitiesSchema.optional().meta({ ...Unrestricted6, ...ClientSafe }),
4073
+ /** Server-evaluated built-in segment dimensions (plan 279 PD-3). */
4074
+ builtin_dimensions: ClientContextBuiltinDimensionsSchema.optional().meta({ ...Unrestricted6, ...ClientSafe })
4010
4075
  }).meta(
4011
4076
  { id: "ClientContext", "x-revturbine-schema-persistence": Transient6, "x-revturbine-schema-exposure": Internal5 }
4012
4077
  );
@@ -4878,6 +4943,91 @@ var AnalyticsMetricLayerSchema = z14.enum(["primitive", "derived"]).meta(meta("A
4878
4943
  var AnalyticsCatalogStatusSchema = z14.enum(["declared", "bound", "validated", "tested", "unavailable"]).meta(meta("AnalyticsCatalogStatus"));
4879
4944
  var AnalyticsFactKindSchema = z14.enum(["transaction", "periodic_snapshot", "accumulating_snapshot"]).meta(meta("AnalyticsFactKind"));
4880
4945
  var AnalyticsConceptProducerSchema = z14.enum(["platform", "customer_authored", "simulation"]).meta(meta("AnalyticsConceptProducer"));
4946
+ var AnalyticsRecordDedupSchema = z14.enum(["deduplicated_on_record_id", "summed_on_sorting_key", "append_only", "upsert_on_primary_key", "recomputed_at_query", "unknown"]).meta(meta("AnalyticsRecordDedup"));
4947
+ var AnalyticsRecordRevisionSchema = z14.enum(["immutable", "re_emit_higher_version", "in_place_update", "recomputed_from_source", "unknown"]).meta(meta("AnalyticsRecordRevision"));
4948
+ var AnalyticsRecordLateArrivalSchema = z14.enum(["accepted", "accepted_within_retention", "rejected", "unknown"]).meta(meta("AnalyticsRecordLateArrival"));
4949
+ var AnalyticsRecordContractSchema = z14.object({
4950
+ /** The columns whose tuple is the stable record id. Identity resolution is never part of it (R2). */
4951
+ record_id: z14.array(z14.string().min(1).max(120)).min(1).max(8).meta(Unrestricted11),
4952
+ /** Where the rows come from: `stripe`, `sdk_clickstream`, `revturbine_control_plane`. */
4953
+ source_namespace: z14.string().min(1).max(120).meta(Unrestricted11),
4954
+ /** Occurred (effective) time. */
4955
+ occurred_time_field: z14.string().min(1).max(120).meta(Unrestricted11),
4956
+ /** Received (observation) time; absent when the source records none. */
4957
+ received_time_field: z14.string().min(1).max(120).optional().meta(Unrestricted11),
4958
+ dedup: AnalyticsRecordDedupSchema.meta(Unrestricted11),
4959
+ revision: AnalyticsRecordRevisionSchema.meta(Unrestricted11),
4960
+ late_arrival: AnalyticsRecordLateArrivalSchema.meta(Unrestricted11),
4961
+ correlation_keys: z14.array(z14.string().min(1).max(120)).max(12).optional().meta(Unrestricted11),
4962
+ /**
4963
+ * The field carrying Stripe live/test mode. R2 requires it on every
4964
+ * billing fact; a billing concept that declares `livemode_qualified: true`
4965
+ * must name it, and one that cannot is `livemode_qualified: false` with
4966
+ * the gap stated in `note`.
4967
+ */
4968
+ livemode_field: z14.string().min(1).max(120).optional().meta(Unrestricted11),
4969
+ /** Stamped policy-version fields (R2/R9). Empty or absent = none stamped. */
4970
+ policy_version_fields: z14.array(z14.string().min(1).max(120)).max(8).optional().meta(Unrestricted11),
4971
+ note: z14.string().max(400).optional().meta(Unrestricted11)
4972
+ }).meta(meta("AnalyticsRecordContract"));
4973
+ var AnalyticsKeyResolutionSchema = z14.enum(["source", "enriched", "absent"]).meta(meta("AnalyticsKeyResolution"));
4974
+ var AnalyticsConceptKeySchema = z14.object({
4975
+ key: z14.string().regex(/^[a-z][a-z0-9_]{0,49}$/).meta(Unrestricted11),
4976
+ /** `column:<name>`, `payload:<field>`, `envelope:<field>` - or `-` when absent. */
4977
+ path: z14.string().min(1).max(200).meta(Unrestricted11),
4978
+ resolution: AnalyticsKeyResolutionSchema.meta(Unrestricted11),
4979
+ note: z14.string().max(400).optional().meta(Unrestricted11)
4980
+ }).meta(meta("AnalyticsConceptKey"));
4981
+ var AnalyticsMeasureUnitSchema = z14.enum(["currency_minor", "count", "seconds", "ratio", "timestamp", "usage_unit", "polymorphic"]).meta(meta("AnalyticsMeasureUnit"));
4982
+ var AnalyticsMeasureTemporalSchema = z14.enum(["stock", "flow", "gauge"]).meta(meta("AnalyticsMeasureTemporal"));
4983
+ var AnalyticsMeasureSignSchema = z14.enum(["signed", "positive_magnitude", "unsigned"]).meta(meta("AnalyticsMeasureSign"));
4984
+ var AnalyticsConceptMeasureSchema = z14.object({
4985
+ name: z14.string().regex(/^[a-z][a-z0-9_]{0,63}$/).meta(Unrestricted11),
4986
+ /** `column:<name>` or `row_count` - a transaction fact whose measure IS its row count. */
4987
+ source: z14.string().min(1).max(200).meta(Unrestricted11),
4988
+ unit: AnalyticsMeasureUnitSchema.meta(Unrestricted11),
4989
+ temporal: AnalyticsMeasureTemporalSchema.meta(Unrestricted11),
4990
+ sign: AnalyticsMeasureSignSchema.meta(Unrestricted11),
4991
+ aggregation_semantics: AnalyticsMetricAggregationSemanticsSchema.meta(Unrestricted11),
4992
+ note: z14.string().max(400).optional().meta(Unrestricted11)
4993
+ }).meta(meta("AnalyticsConceptMeasure"));
4994
+ var AnalyticsConceptEventAttributeSchema = z14.object({
4995
+ name: z14.string().regex(/^[a-z][a-z0-9_]{0,63}$/).meta(Unrestricted11),
4996
+ /** `column:<name>`, `payload:<field>` or `envelope:<field>`. */
4997
+ path: z14.string().min(1).max(200).meta(Unrestricted11),
4998
+ note: z14.string().max(400).optional().meta(Unrestricted11)
4999
+ }).meta(meta("AnalyticsConceptEventAttribute"));
5000
+ var AnalyticsConceptSourceKindSchema = z14.enum(["ingested_event", "datasource", "control_plane_table", "build", "unavailable"]).meta(meta("AnalyticsConceptSourceKind"));
5001
+ var AnalyticsConceptSourceSchema = z14.object({
5002
+ kind: AnalyticsConceptSourceKindSchema.meta(Unrestricted11),
5003
+ ref: z14.string().min(1).max(200).meta(Unrestricted11),
5004
+ /** Required when `kind` is `unavailable`: what has to ship before rows exist. */
5005
+ blocker: z14.string().max(400).optional().meta(Unrestricted11),
5006
+ note: z14.string().max(400).optional().meta(Unrestricted11)
5007
+ }).meta(meta("AnalyticsConceptSource"));
5008
+ var AnalyticsConceptBuildSchema = z14.object({
5009
+ kind: z14.enum(["pipe", "materialized_view", "worker", "query_time", "unavailable"]).meta(Unrestricted11),
5010
+ ref: z14.string().min(1).max(200).meta(Unrestricted11),
5011
+ inputs: z14.array(z14.string().min(1).max(200)).min(1).max(12).meta(Unrestricted11),
5012
+ blocker: z14.string().max(400).optional().meta(Unrestricted11)
5013
+ }).meta(meta("AnalyticsConceptBuild"));
5014
+ var AnalyticsConceptMaturitySchema = z14.object({
5015
+ status: z14.enum(["declared", "unavailable"]).meta(Unrestricted11),
5016
+ horizon_field: z14.string().min(1).max(120).optional().meta(Unrestricted11),
5017
+ observed_through_field: z14.string().min(1).max(120).optional().meta(Unrestricted11),
5018
+ blocker: z14.string().max(400).optional().meta(Unrestricted11)
5019
+ }).meta(meta("AnalyticsConceptMaturity"));
5020
+ var AnalyticsOracleFamilySchema = z14.enum(["gate", "presentation", "revenue", "attribution", "event_count", "cohort", "funnel", "read_through", "none"]).meta(meta("AnalyticsOracleFamily"));
5021
+ var AnalyticsConceptOracleFamilySchema = z14.object({
5022
+ family: AnalyticsOracleFamilySchema.meta(Unrestricted11),
5023
+ blocker: z14.string().max(400).optional().meta(Unrestricted11)
5024
+ }).meta(meta("AnalyticsConceptOracleFamily"));
5025
+ var AnalyticsPolicyKindSchema = z14.enum(["mrr", "attribution", "lifecycle", "engagement"]).meta(meta("AnalyticsPolicyKind"));
5026
+ var AnalyticsConceptPolicyDependencySchema = z14.object({
5027
+ kind: AnalyticsPolicyKindSchema.meta(Unrestricted11),
5028
+ stamped: z14.boolean().meta(Unrestricted11),
5029
+ blocker: z14.string().max(400).optional().meta(Unrestricted11)
5030
+ }).meta(meta("AnalyticsConceptPolicyDependency"));
4881
5031
  var AnalyticsGroundingAnchorSchema = z14.enum([
4882
5032
  "fact_time",
4883
5033
  "period_opening",
@@ -5078,13 +5228,20 @@ var AnalyticsCatalogConceptSchema = z14.object({
5078
5228
  * exposure, not per placement-day — a daily rollup is a pipe's physical
5079
5229
  * choice, not a second grain).
5080
5230
  *
5081
- * These fields are optional while the concepts migrate onto them. They
5082
- * are NOT decorative: a concept that declares `fact_kind` is asserting it
5083
- * has been mapped, and the structural rules tighten around it from there.
5084
- * Populating them for the existing concepts, and splitting `revenue.movement`
5085
- * into `revenue.stock` / `.movement` / `.ledger`, rides with the fact-table
5086
- * builds that produce the rows (later plan 252 tasks and plan 230) rather
5087
- * than being invented here.
5231
+ * These fields stay optional on the canonical schema so a partial fixture
5232
+ * still parses, but they are NOT decorative: a concept that declares
5233
+ * `fact_kind` is asserting it has been mapped, and
5234
+ * `AnalyticsCatalogConceptValidatedSchema` then REQUIRES the whole set —
5235
+ * the same split as the metric schema. Every shipped concept declares them
5236
+ * (BL-0246, TASK-58's second half).
5237
+ *
5238
+ * `revenue.movement` is still NOT split into `revenue.stock` /
5239
+ * `.movement` / `.ledger`: today's definitions do not distinguish them
5240
+ * (the only served measure is the invoice-paid cash proxy), so the split
5241
+ * stays registered as the `catalog_status: 'unavailable'` blocker on
5242
+ * `revenue.mrr` / `revenue.net_new_mrr` and rides with the normalized
5243
+ * recurring stock and movement oracle (plan 252 TASK-29/34/36/45) that
5244
+ * produce the rows. Declaring three empty concepts here would invent them.
5088
5245
  */
5089
5246
  fact_kind: AnalyticsFactKindSchema.optional().meta(Unrestricted11),
5090
5247
  family: z14.enum(["billing", "behavioral"]).optional().meta(Unrestricted11),
@@ -5098,8 +5255,286 @@ var AnalyticsCatalogConceptSchema = z14.object({
5098
5255
  }).optional().meta(Unrestricted11),
5099
5256
  /** Billing facts carry Stripe live/test mode; without it live and test revenue are indistinguishable. */
5100
5257
  livemode_qualified: z14.boolean().optional().meta(Unrestricted11),
5258
+ /** The record contract the rows obey (R2). */
5259
+ record_contract: AnalyticsRecordContractSchema.optional().meta(Unrestricted11),
5260
+ /** Every entity key the fact carries, and the path it resolves through (R1). */
5261
+ keys: z14.array(AnalyticsConceptKeySchema).max(24).optional().meta(Unrestricted11),
5262
+ /**
5263
+ * The stored measures. May be EMPTY for a concept every one of whose
5264
+ * metrics is `catalog_status: 'unavailable'` — there is no measure to
5265
+ * declare until the fact exists. The in-memory integrity check enforces
5266
+ * that pairing, so an empty list cannot hide a served metric.
5267
+ */
5268
+ measures: z14.array(AnalyticsConceptMeasureSchema).max(32).optional().meta(Unrestricted11),
5269
+ /** Non-key attributes carried on the row (R7). */
5270
+ event_attributes: z14.array(AnalyticsConceptEventAttributeSchema).max(32).optional().meta(Unrestricted11),
5271
+ /** Where the rows come from (R1/R5). */
5272
+ sources: z14.array(AnalyticsConceptSourceSchema).min(1).max(12).optional().meta(Unrestricted11),
5273
+ /** How snapshot rows are built (R5); required for both snapshot kinds. */
5274
+ build: AnalyticsConceptBuildSchema.optional().meta(Unrestricted11),
5275
+ /** Horizon / `observed_through` right-censoring (R5); required for accumulating snapshots. */
5276
+ maturity: AnalyticsConceptMaturitySchema.optional().meta(Unrestricted11),
5277
+ /** The pure oracle family that derives this concept's metrics (R6). */
5278
+ oracle_family: AnalyticsConceptOracleFamilySchema.optional().meta(Unrestricted11),
5279
+ /** The versioned policy kinds the rows depend on, and whether the version is stamped (R9). */
5280
+ policy_dependencies: z14.array(AnalyticsConceptPolicyDependencySchema).max(8).optional().meta(Unrestricted11),
5101
5281
  deprecation: CatalogDeprecation.optional().meta(Unrestricted11)
5102
5282
  }).meta(meta("AnalyticsCatalogConcept"));
5283
+ var AnalyticsCatalogConceptValidatedSchema = AnalyticsCatalogConceptSchema.superRefine(
5284
+ (concept, ctx) => {
5285
+ const problem = (path3, code, message) => {
5286
+ ctx.addIssue({ code: "custom", path: path3, params: { code }, message });
5287
+ };
5288
+ const duplicates = (names) => {
5289
+ const seen = /* @__PURE__ */ new Set();
5290
+ const dupes = /* @__PURE__ */ new Set();
5291
+ for (const name of names) {
5292
+ if (seen.has(name)) dupes.add(name);
5293
+ seen.add(name);
5294
+ }
5295
+ return [...dupes].sort();
5296
+ };
5297
+ if (concept.fact_kind === void 0) {
5298
+ for (const field of [
5299
+ "family",
5300
+ "primary_key",
5301
+ "producer",
5302
+ "materialization",
5303
+ "livemode_qualified",
5304
+ "record_contract",
5305
+ "keys",
5306
+ "measures",
5307
+ "event_attributes",
5308
+ "build",
5309
+ "maturity"
5310
+ ]) {
5311
+ if (concept[field] !== void 0) {
5312
+ problem(
5313
+ [field],
5314
+ "fact_table_field_without_fact_kind",
5315
+ `${field} describes a fact table's rows, so the concept must declare fact_kind`
5316
+ );
5317
+ }
5318
+ }
5319
+ for (const [index, source] of (concept.sources ?? []).entries()) {
5320
+ if (source.kind !== "unavailable") {
5321
+ problem(
5322
+ ["sources", index, "kind"],
5323
+ "real_source_without_fact_kind",
5324
+ "a concept naming a real row source must declare fact_kind (D-9: the concept IS the fact table)"
5325
+ );
5326
+ }
5327
+ }
5328
+ } else {
5329
+ const required = [
5330
+ "family",
5331
+ "primary_key",
5332
+ "producer",
5333
+ "materialization",
5334
+ "livemode_qualified",
5335
+ "record_contract",
5336
+ "keys",
5337
+ "measures",
5338
+ "event_attributes",
5339
+ "sources",
5340
+ "oracle_family",
5341
+ "policy_dependencies"
5342
+ ];
5343
+ for (const field of required) {
5344
+ if (concept[field] === void 0) {
5345
+ problem(
5346
+ [field],
5347
+ "mapped_concept_missing_fact_table_field",
5348
+ `a concept declaring fact_kind must declare ${field} (D-9: the concept IS the fact table)`
5349
+ );
5350
+ }
5351
+ }
5352
+ if (concept.keys !== void 0 && concept.keys.length === 0) {
5353
+ problem(["keys"], "mapped_concept_without_keys", "a fact carries at least one entity key (R1)");
5354
+ }
5355
+ for (const [dimensionId, grounding] of Object.entries(concept.dimension_groundings ?? {})) {
5356
+ if (grounding.anchor === void 0) {
5357
+ problem(
5358
+ ["dimension_groundings", dimensionId, "anchor"],
5359
+ "grounding_without_anchor",
5360
+ `grounding for ${dimensionId} must declare an anchor once the concept declares fact_kind (R8)`
5361
+ );
5362
+ }
5363
+ }
5364
+ if (concept.fact_kind === "accumulating_snapshot" && concept.maturity === void 0) {
5365
+ problem(
5366
+ ["maturity"],
5367
+ "accumulating_snapshot_without_maturity",
5368
+ "an accumulating snapshot must declare maturity \u2014 status 'unavailable' with a blocker when right-censoring is not implemented (R5)"
5369
+ );
5370
+ }
5371
+ if (concept.fact_kind !== "transaction" && concept.build === void 0) {
5372
+ problem(["build"], "snapshot_without_build", "a snapshot fact must declare the build that produces its rows (R5)");
5373
+ }
5374
+ if (concept.fact_kind === "transaction" && concept.maturity !== void 0) {
5375
+ problem(["maturity"], "transaction_with_maturity", "maturity is a snapshot property; a transaction fact has no horizon (R5)");
5376
+ }
5377
+ }
5378
+ for (const [dimensionId, grounding] of Object.entries(concept.dimension_groundings ?? {})) {
5379
+ if (grounding.catalog_status === "unavailable" && !grounding.blocker?.trim()) {
5380
+ problem(
5381
+ ["dimension_groundings", dimensionId, "blocker"],
5382
+ "unavailable_grounding_without_blocker",
5383
+ `grounding for ${dimensionId} is catalog_status 'unavailable' and must name the blocker (R11/G13)`
5384
+ );
5385
+ }
5386
+ if (grounding.catalog_status !== "unavailable" && grounding.blocker !== void 0) {
5387
+ problem(
5388
+ ["dimension_groundings", dimensionId, "blocker"],
5389
+ "grounding_blocker_without_unavailable",
5390
+ `grounding for ${dimensionId} carries a blocker, so its catalog_status must be 'unavailable' (R11/G13)`
5391
+ );
5392
+ }
5393
+ if (grounding.kind !== "config_join" && grounding.kind !== "membership_join") continue;
5394
+ if (grounding.catalog_status === "unavailable") continue;
5395
+ const joined = (concept.sources ?? []).some(
5396
+ (source) => source.ref === grounding.source && source.kind !== "unavailable"
5397
+ );
5398
+ if (!joined) {
5399
+ problem(
5400
+ ["dimension_groundings", dimensionId, "source"],
5401
+ "join_grounding_without_execution_path",
5402
+ `grounding for ${dimensionId} is a ${grounding.kind} against '${grounding.source}', which the concept does not declare among its sources \u2014 nothing performs that join, so the grounding must declare catalog_status 'unavailable' with its blocker (R11/G13)`
5403
+ );
5404
+ }
5405
+ }
5406
+ if (concept.materialization?.mode === "rollup" && concept.materialization.rollup_grain === void 0) {
5407
+ problem(["materialization", "rollup_grain"], "rollup_without_grain", "materialization mode 'rollup' must name its rollup_grain");
5408
+ }
5409
+ if (concept.materialization && concept.materialization.mode !== "rollup" && concept.materialization.rollup_grain !== void 0) {
5410
+ problem(["materialization", "rollup_grain"], "rollup_grain_without_rollup", "rollup_grain is only meaningful when mode is 'rollup'");
5411
+ }
5412
+ if (concept.family === "billing" && concept.livemode_qualified === void 0) {
5413
+ problem(["livemode_qualified"], "billing_concept_without_livemode", "a billing fact must state whether it is live/test qualified (R2)");
5414
+ }
5415
+ if (concept.livemode_qualified === true && concept.record_contract?.livemode_field === void 0) {
5416
+ problem(
5417
+ ["record_contract", "livemode_field"],
5418
+ "livemode_qualified_without_field",
5419
+ "livemode_qualified: true must name the field carrying Stripe live/test mode (R2)"
5420
+ );
5421
+ }
5422
+ for (const [index, key2] of (concept.keys ?? []).entries()) {
5423
+ if (key2.resolution === "absent" && !key2.note?.trim()) {
5424
+ problem(["keys", index, "note"], "absent_key_without_note", "resolution 'absent' must state why the key is not carried (R1)");
5425
+ }
5426
+ if (key2.resolution === "absent" && key2.path !== "-") {
5427
+ problem(["keys", index, "path"], "absent_key_with_path", "resolution 'absent' must carry path '-' \u2014 there is no path to read");
5428
+ }
5429
+ if (key2.resolution !== "absent" && key2.path === "-") {
5430
+ problem(["keys", index, "path"], "resolved_key_without_path", "a source or enriched key must name the path it resolves through");
5431
+ }
5432
+ }
5433
+ for (const duplicate of duplicates((concept.keys ?? []).map((key2) => key2.key))) {
5434
+ problem(["keys"], "duplicate_key", `duplicate entity key: ${duplicate}`);
5435
+ }
5436
+ for (const [index, measure] of (concept.measures ?? []).entries()) {
5437
+ if (measure.temporal === "stock" && measure.aggregation_semantics !== "semi_additive") {
5438
+ problem(
5439
+ ["measures", index, "aggregation_semantics"],
5440
+ "stock_measure_not_semi_additive",
5441
+ "a stock measure is semi_additive by construction \u2014 it is summed at one date, never across dates (R3)"
5442
+ );
5443
+ }
5444
+ if (measure.unit === "polymorphic") {
5445
+ if (!measure.note?.trim()) {
5446
+ problem(
5447
+ ["measures", index, "note"],
5448
+ "polymorphic_measure_without_note",
5449
+ "unit 'polymorphic' must name the column that decides the unit"
5450
+ );
5451
+ }
5452
+ if (measure.aggregation_semantics !== "non_additive") {
5453
+ problem(
5454
+ ["measures", index, "aggregation_semantics"],
5455
+ "polymorphic_measure_summed",
5456
+ "unit 'polymorphic' is non_additive \u2014 rows of different units cannot be summed"
5457
+ );
5458
+ }
5459
+ }
5460
+ if (measure.unit === "count" && measure.sign === "signed") {
5461
+ problem(["measures", index, "sign"], "signed_count_measure", "a count is unsigned; a signed change is a currency or usage delta (R3)");
5462
+ }
5463
+ }
5464
+ for (const duplicate of duplicates((concept.measures ?? []).map((measure) => measure.name))) {
5465
+ problem(["measures"], "duplicate_measure", `duplicate measure name: ${duplicate}`);
5466
+ }
5467
+ for (const duplicate of duplicates((concept.event_attributes ?? []).map((attribute) => attribute.name))) {
5468
+ problem(["event_attributes"], "duplicate_event_attribute", `duplicate event attribute: ${duplicate}`);
5469
+ }
5470
+ for (const [index, source] of (concept.sources ?? []).entries()) {
5471
+ if (source.kind === "unavailable" && !source.blocker?.trim()) {
5472
+ problem(["sources", index, "blocker"], "unavailable_source_without_blocker", "source kind 'unavailable' must name the blocker");
5473
+ }
5474
+ if (source.kind !== "unavailable" && source.blocker !== void 0) {
5475
+ problem(["sources", index, "blocker"], "available_source_with_blocker", "a real source carries no blocker \u2014 use note");
5476
+ }
5477
+ }
5478
+ const sourceKinds = new Set((concept.sources ?? []).map((source) => source.kind));
5479
+ if (sourceKinds.has("unavailable") && sourceKinds.size > 1) {
5480
+ problem(["sources"], "mixed_unavailable_sources", "a concept with an 'unavailable' source declares no other source");
5481
+ }
5482
+ if (sourceKinds.has("unavailable") && (concept.measures?.length ?? 0) > 0) {
5483
+ problem(["measures"], "unavailable_source_with_measures", "a concept with no fact declares no measures");
5484
+ }
5485
+ if (concept.build?.kind === "unavailable" && !concept.build.blocker?.trim()) {
5486
+ problem(["build", "blocker"], "unavailable_build_without_blocker", "build kind 'unavailable' must name the blocker");
5487
+ }
5488
+ if (concept.build && concept.build.kind !== "unavailable" && concept.build.blocker !== void 0) {
5489
+ problem(["build", "blocker"], "available_build_with_blocker", "a real build carries no blocker");
5490
+ }
5491
+ if (concept.maturity) {
5492
+ if (concept.maturity.status === "declared") {
5493
+ for (const field of ["horizon_field", "observed_through_field"]) {
5494
+ if (concept.maturity[field] === void 0) {
5495
+ problem(["maturity", field], "declared_maturity_missing_field", `maturity status 'declared' must name ${field} (R5)`);
5496
+ }
5497
+ }
5498
+ if (concept.maturity.blocker !== void 0) {
5499
+ problem(["maturity", "blocker"], "declared_maturity_with_blocker", "maturity status 'declared' carries no blocker");
5500
+ }
5501
+ } else if (!concept.maturity.blocker?.trim()) {
5502
+ problem(["maturity", "blocker"], "unavailable_maturity_without_blocker", "maturity status 'unavailable' must name the blocker");
5503
+ }
5504
+ }
5505
+ if (concept.oracle_family) {
5506
+ if (concept.oracle_family.family === "none" && !concept.oracle_family.blocker?.trim()) {
5507
+ problem(["oracle_family", "blocker"], "no_oracle_without_blocker", "oracle_family 'none' must name why no oracle covers this concept (R6)");
5508
+ }
5509
+ if (concept.oracle_family.family !== "none" && concept.oracle_family.blocker !== void 0) {
5510
+ problem(["oracle_family", "blocker"], "oracle_family_with_blocker", "a concept with an oracle family carries no blocker");
5511
+ }
5512
+ }
5513
+ for (const [index, dependency] of (concept.policy_dependencies ?? []).entries()) {
5514
+ if (!dependency.stamped && !dependency.blocker?.trim()) {
5515
+ problem(
5516
+ ["policy_dependencies", index, "blocker"],
5517
+ "unstamped_policy_without_blocker",
5518
+ `policy dependency '${dependency.kind}' is not stamped on the row, so it must name the blocker (R9)`
5519
+ );
5520
+ }
5521
+ if (dependency.stamped && dependency.blocker !== void 0) {
5522
+ problem(["policy_dependencies", index, "blocker"], "stamped_policy_with_blocker", "a stamped policy dependency carries no blocker");
5523
+ }
5524
+ }
5525
+ for (const duplicate of duplicates((concept.policy_dependencies ?? []).map((dependency) => dependency.kind))) {
5526
+ problem(["policy_dependencies"], "duplicate_policy_dependency", `duplicate policy dependency: ${duplicate}`);
5527
+ }
5528
+ const stampedPolicies = (concept.policy_dependencies ?? []).filter((dependency) => dependency.stamped);
5529
+ if (stampedPolicies.length > 0 && (concept.record_contract?.policy_version_fields?.length ?? 0) === 0) {
5530
+ problem(
5531
+ ["record_contract", "policy_version_fields"],
5532
+ "stamped_policy_without_version_field",
5533
+ "a stamped policy dependency requires the record contract to name the policy-version field it is stamped in (R2)"
5534
+ );
5535
+ }
5536
+ }
5537
+ ).meta(meta("AnalyticsCatalogConceptValidated"));
5103
5538
  var OrderBy = z14.strictObject({
5104
5539
  field: SemanticIdField.meta(Unrestricted11),
5105
5540
  direction: z14.enum(["asc", "desc"]).meta(Unrestricted11)
@@ -5732,6 +6167,51 @@ var MilestonePayload = z18.looseObject({});
5732
6167
  var AcquisitionMilestonePayload = z18.looseObject({
5733
6168
  acquisition_source: z18.string().optional().meta(Unrestricted15)
5734
6169
  });
6170
+ var LifecycleEvidence = z18.looseObject({
6171
+ kind: z18.enum(["provider_fact", "app_fact", "usage_exhaustion"]).meta(Unrestricted15),
6172
+ /** The producer-scoped reference to the proving fact (a Stripe event id, an
6173
+ * app write id, a metering observation id). Opaque to RevTurbine. */
6174
+ ref: str()
6175
+ });
6176
+ var AccountCreatedPayload = z18.looseObject({
6177
+ acquisition_source: z18.string().optional().meta(Unrestricted15),
6178
+ /** The account grain (plan 276 R-2) — never a user id. */
6179
+ account_id: onstr(),
6180
+ /** When the account came into existence, per the producer. */
6181
+ created_at: onstr(),
6182
+ /** What kind of act created it (e.g. `self_serve_signup`, `invite_accepted`,
6183
+ * `provisioned`, `import`). A USER signup is not account creation (R-2). */
6184
+ source: onstr(),
6185
+ evidence: LifecycleEvidence.nullable().optional()
6186
+ });
6187
+ var TrialRevisionPayload = z18.looseObject({
6188
+ /** Stable id for the episode this revision belongs to. Conversion links only
6189
+ * its OWN episode (plan 276 AC-4), so this is half the identity key. */
6190
+ trial_episode_id: str(),
6191
+ /** The account the episode is materialized against (R-2). */
6192
+ account_id: str(),
6193
+ /** `user` retains a user-subject episode WITHOUT making it an account-wide
6194
+ * grant (R-2); `account` is an account-wide grant. */
6195
+ subject_scope: z18.enum(["account", "user"]).meta(Unrestricted15),
6196
+ /** The Playbook trial rule's `unique_handle`, when the episode came from one. */
6197
+ rule_handle: onstr(),
6198
+ /** The trialed plan's handle, when the episode grants one plan. */
6199
+ plan_handle: onstr(),
6200
+ /** `free_trial` / `reverse_trial` / anything the tenant runs. Vocabulary only. */
6201
+ trial_type: onstr(),
6202
+ revision: z18.enum(["started", "extended", "converted", "reverted", "expired", "revoked"]).meta(Unrestricted15),
6203
+ /** When the revision took effect, per the proving fact — never the write clock. */
6204
+ effective_at: str(),
6205
+ /** The declared end. Its passing proves only that the time passed (R-1(c)). */
6206
+ scheduled_end_at: onstr(),
6207
+ /** The EVIDENCED end. Null until a fact closes the episode. */
6208
+ actual_end_at: onstr(),
6209
+ evidence: LifecycleEvidence,
6210
+ /** The biller's reference when a provider fact is involved (e.g. a Stripe
6211
+ * subscription id), so an app fact and a provider derivative about the same
6212
+ * episode can be reconciled. */
6213
+ provider_ref: onstr()
6214
+ });
5735
6215
  var UsageRecordedPayload = z18.looseObject({
5736
6216
  metered_units: z18.number().nonnegative().meta(Unrestricted15),
5737
6217
  included_allowance: z18.number().nonnegative().meta(Unrestricted15),
@@ -5869,7 +6349,7 @@ var EVENT_PAYLOAD_CONTRACTS = {
5869
6349
  user_context_observed: { schema: UserContextObservedPayload, identity: envelopeIdentity },
5870
6350
  clickstream_page_view: { schema: PageViewPayload, identity: envelopeIdentity },
5871
6351
  // SDK client — customer-product lifecycle milestones (promoted, R-1)
5872
- account_created: { schema: AcquisitionMilestonePayload, identity: envelopeIdentity },
6352
+ account_created: { schema: AccountCreatedPayload, identity: envelopeIdentity },
5873
6353
  user_signed_up: { schema: AcquisitionMilestonePayload, identity: envelopeIdentity },
5874
6354
  onboarding_completed: { schema: MilestonePayload, identity: envelopeIdentity },
5875
6355
  account_activated: { schema: MilestonePayload, identity: envelopeIdentity },
@@ -5879,6 +6359,10 @@ var EVENT_PAYLOAD_CONTRACTS = {
5879
6359
  plan_viewed: { schema: PlanViewedPayload, identity: envelopeIdentity },
5880
6360
  promotion_applied: { schema: PromotionAppliedPayload, identity: envelopeIdentity },
5881
6361
  promotion_converted: { schema: PromotionConvertedPayload, identity: envelopeIdentity },
6362
+ // App-owned trial episode (plan 276 R-1(b)) — identity is the episode plus
6363
+ // the revision, so a retry of the same app fact collapses while a later
6364
+ // revision of the same episode stays a distinct occurrence.
6365
+ trial_revision: { schema: TrialRevisionPayload, identity: ["trial_episode_id", "revision"] },
5882
6366
  // SDK server — derived observations
5883
6367
  growth_signal_observed: { schema: GrowthSignalObservedPayload, identity: envelopeIdentity },
5884
6368
  // SDK meta lane
@@ -5998,7 +6482,15 @@ var SDK_CLIENT_EVENT_NAMES = [
5998
6482
  "usage_recorded",
5999
6483
  "plan_viewed",
6000
6484
  "promotion_applied",
6001
- "promotion_converted"
6485
+ "promotion_converted",
6486
+ // App-owned trial-episode revision (plan 276 R-1(b), TASK-12/TASK-13; ruling
6487
+ // D-21). The authoritative fact for an APP-RUN trial: the tenant's product
6488
+ // owns trial execution, so only the tenant's product can state that an
6489
+ // episode started, was extended, converted, reverted, expired or was
6490
+ // revoked. Distinct from the `trial_*` billing band, which is the provider
6491
+ // derivative (R-1(a)) and cannot establish enrolment. The clock is never an
6492
+ // authority (R-1(c)): an elapsed scheduled end emits nothing at all.
6493
+ "trial_revision"
6002
6494
  ];
6003
6495
  var SDK_SERVER_EVENT_NAMES = [
6004
6496
  "growth_signal_observed"
@@ -6079,7 +6571,7 @@ var EVENT_METADATA = {
6079
6571
  sdk_validation_warning: { purpose: "The SDK found a config or usage problem worth surfacing.", stability: "internal" },
6080
6572
  resolution_failure: { purpose: "A decision produced nothing; allow-listed handles and closed reason codes only.", stability: "internal" },
6081
6573
  // Customer-product lifecycle milestones (plan 228 TASK-2, promoted under R-1)
6082
- account_created: { purpose: "A customer account came into existence in the tenant product; acquisition_source rides on the payload.", stability: "stable" },
6574
+ account_created: { purpose: "A customer account came into existence in the tenant product; acquisition_source rides on the payload, and the plan-276 REQ-3 account-creation evidence (account_id, created_at, source, evidence) rides on it when the producer holds it.", stability: "stable" },
6083
6575
  user_signed_up: { purpose: "A user completed signup in the tenant product.", stability: "stable" },
6084
6576
  onboarding_completed: { purpose: "A user finished the tenant product onboarding flow.", stability: "stable" },
6085
6577
  account_activated: { purpose: "An account reached the tenant-defined activation milestone.", stability: "stable" },
@@ -6089,6 +6581,7 @@ var EVENT_METADATA = {
6089
6581
  plan_viewed: { purpose: "A user viewed plan or pricing content; plan_handle and entry_tier ride on the payload.", stability: "stable" },
6090
6582
  promotion_applied: { purpose: "A promotion was applied to an account; promotion_handle rides on the payload.", stability: "stable" },
6091
6583
  promotion_converted: { purpose: "An account with a promotion converted; full_price marks conversions where the discount lapsed first.", stability: "stable" },
6584
+ trial_revision: { purpose: "The app-owned authoritative fact about one trial episode (plan 276 R-1(b)): which revision occurred (started/extended/converted/reverted/expired/revoked), when it took effect, and the evidence that proves it. Trial execution and ownership stay with the customer app.", stability: "stable" },
6092
6585
  // Server-derived observations (plan 228 TASK-2)
6093
6586
  growth_signal_observed: { purpose: "A pre-computed windowed metric observation written by a server-side producer; readers should prefer read-time derivation where a platform source exists.", stability: "internal" },
6094
6587
  // Billing lifecycle (plan 228 R-2) — dual-source: Stripe webhook processor OR typed SDK emit.
@@ -6467,7 +6960,29 @@ var EventEnvelopeSchema = IdField.extend({
6467
6960
  event_type: z24.string().min(1).meta(Unrestricted20),
6468
6961
  source: EventSourceSchema.default("sdk").meta(Unrestricted20),
6469
6962
  tenant_id: z24.string().min(1).optional().meta(Unrestricted20),
6470
- user_id: z24.string().min(1).optional().meta(Pii4),
6963
+ /**
6964
+ * Identity keys are `Unrestricted` on EVERY lane (BL-0131, Kent's **D-19**,
6965
+ * 2026-09-25): *"Not treated as PII, but a PII leak still detectable. We
6966
+ * want our customers to use UUIDs, but if they pass an email, we need to
6967
+ * hash it consistently."*
6968
+ *
6969
+ * A `user_id` / `account_id` is the TENANT'S OWN primary key, not personal
6970
+ * data. Before BL-0131 the three ingest lanes disagreed — `TrackEvent.user_id`
6971
+ * and `TreatmentInteractionInput.account_id` were `Pii` while
6972
+ * `TrackEvent.account_id` was `Unrestricted` — so the same logical key
6973
+ * carried two classifications depending on which route it arrived through.
6974
+ *
6975
+ * The leak is handled at the BOUNDARY, not by the classification:
6976
+ * `classifyIdentityValue` / `normalizeIdentityValue` in `@revt-eng/core`
6977
+ * detect a PII-shaped value, consistently hash an email-shaped one
6978
+ * (`eml_<sha256-16>`, unsalted so every writer and reader agrees), and
6979
+ * report `identity_pii_detected` so the tenant learns to send UUIDs.
6980
+ *
6981
+ * Dropping `pii` does NOT widen browser exposure: the plan-157
6982
+ * client-context filter is an allowlist keyed on field-level `external`
6983
+ * exposure, which these fields do not carry, so they stay `server_only`.
6984
+ */
6985
+ user_id: z24.string().min(1).optional().meta(Unrestricted20),
6471
6986
  session_id: z24.string().min(1).optional().meta(Pii4),
6472
6987
  occurred_at: z24.string().datetime().meta(Unrestricted20),
6473
6988
  request_id: z24.string().min(1).meta(Unrestricted20),
@@ -6495,7 +7010,9 @@ var EventIngestBatchSchema = z24.array(
6495
7010
  event_type: z24.string().min(1).meta(Unrestricted20),
6496
7011
  occurred_at: z24.string().datetime().optional().meta(Unrestricted20),
6497
7012
  tenant_id: z24.string().min(1).optional().meta(Unrestricted20),
6498
- user_id: z24.string().min(1).optional().meta(Pii4),
7013
+ // Identity key — `Unrestricted` on every lane (BL-0131, D-19); see
7014
+ // `EventEnvelopeSchema.user_id`.
7015
+ user_id: z24.string().min(1).optional().meta(Unrestricted20),
6499
7016
  session_id: z24.string().min(1).optional().meta(Pii4),
6500
7017
  attributes: z24.record(z24.string(), z24.unknown()).optional().meta(Unrestricted20),
6501
7018
  payload: z24.record(z24.string(), z24.unknown()).optional().meta(Unrestricted20),
@@ -6525,7 +7042,9 @@ var TreatmentInteractionTypeSchema = z24.enum([
6525
7042
  }
6526
7043
  );
6527
7044
  var TreatmentInteractionInputSchema = z24.object({
6528
- user_id: z24.string().min(1).meta(Pii4),
7045
+ // Identity key — `Unrestricted` on every lane (BL-0131, D-19); see
7046
+ // `EventEnvelopeSchema.user_id` for the full rationale.
7047
+ user_id: z24.string().min(1).meta(Unrestricted20),
6529
7048
  /**
6530
7049
  * The account / organization the user acted on behalf of (plan 232 REQ-4).
6531
7050
  *
@@ -6540,8 +7059,13 @@ var TreatmentInteractionInputSchema = z24.object({
6540
7059
  * Optional, and the route keeps the `user_id` fallback when it is absent,
6541
7060
  * so pre-232 SDKs behave exactly as before rather than writing an empty
6542
7061
  * key that the `account_id != ''` predicates would silently drop.
7062
+ *
7063
+ * BL-0131 (D-19) aligned this to `Unrestricted`, matching the clickstream
7064
+ * half (`TrackEvent.account_id`): an account id is a join key, not
7065
+ * personal data, and a PII-shaped value is detected and consistently
7066
+ * hashed at the ingest boundary instead of being re-labelled here.
6543
7067
  */
6544
- account_id: z24.string().min(1).optional().meta(Pii4),
7068
+ account_id: z24.string().min(1).optional().meta(Unrestricted20),
6545
7069
  placement_id: z24.string().min(1).meta(Unrestricted20),
6546
7070
  treatment_id: z24.string().min(1).optional().meta(Unrestricted20),
6547
7071
  // Presentation context (plan 114) — carried so a treatment interaction can
@@ -6791,7 +7315,9 @@ var EventOriginSchema = z24.enum(["explicit", "automatic", "derived", "raw"]).me
6791
7315
  );
6792
7316
  var TrackEventSchema = z24.object({
6793
7317
  environment_id: z24.string().min(1).meta(Unrestricted20),
6794
- user_id: z24.string().min(1).meta(Pii4),
7318
+ // Identity key — `Unrestricted` on every lane (BL-0131, D-19); see
7319
+ // `EventEnvelopeSchema.user_id` for the full rationale.
7320
+ user_id: z24.string().min(1).meta(Unrestricted20),
6795
7321
  /**
6796
7322
  * The account / organization the user acted on behalf of (BL-0117).
6797
7323
  *
@@ -7506,6 +8032,39 @@ var TrialInstanceSchema = IdField.merge(TimestampFields).merge(TenantIdField).ex
7506
8032
  usage_limit_value: z27.number().int().min(1).optional().meta(Unrestricted21),
7507
8033
  converted_at: NullableDatetimeField.meta(Unrestricted21),
7508
8034
  cancelled_at: NullableDatetimeField.meta(Unrestricted21),
8035
+ /**
8036
+ * Stable identity of the trial EPISODE this row represents
8037
+ * (BL-0247 / plan 276 TASK-12). A customer can run more than one
8038
+ * trial, and a subscription can re-enter `trialing`, so the
8039
+ * (customer, subscription) pair does not name an occurrence. The
8040
+ * billing lane already discriminates an episode by the Stripe
8041
+ * subscription id plus its `trial_start`; this column persists
8042
+ * that same discriminator so a fact about one episode can be
8043
+ * written to exactly that episode's row.
8044
+ *
8045
+ * Nullable: rows created before the column existed, and rows
8046
+ * created by paths that have no Stripe evidence to mint an id
8047
+ * from, carry NULL. NULL means "this episode has no first-class
8048
+ * identity", never "episode zero" — and Postgres treats NULLs as
8049
+ * distinct, so the composite UNIQUE below does not collapse them.
8050
+ */
8051
+ trial_episode_id: z27.string().min(1).nullable().optional().meta(Unrestricted21),
8052
+ /**
8053
+ * The Stripe subscription this episode belongs to — the binding
8054
+ * that lets a Stripe fact resolve one episode without guessing
8055
+ * (BL-0245 carried this in `metadata.stripe_subscription_id`).
8056
+ * Nullable because a trial need not originate from Stripe at all.
8057
+ */
8058
+ stripe_subscription_id: z27.string().min(1).nullable().optional().meta(Unrestricted21),
8059
+ /**
8060
+ * The EVIDENCED end of the episode: the moment a provider fact
8061
+ * said the trial actually stopped. Distinct from `expires_at`,
8062
+ * which is the SCHEDULED end and must stay distinguishable
8063
+ * (plan 276 R-1 — no clock authority; an elapsed `expires_at`
8064
+ * with no fact behind it is not an end). Null until a fact
8065
+ * proves one.
8066
+ */
8067
+ actual_end_at: NullableDatetimeField.meta(Unrestricted21),
7509
8068
  metadata: MetadataField.meta(Unrestricted21)
7510
8069
  }).meta(
7511
8070
  { id: "TrialInstance", "x-revturbine-schema-persistence": Persisted13, "x-revturbine-schema-exposure": Internal17 }
@@ -7708,7 +8267,12 @@ var trialPaths = {
7708
8267
  summary: "List trial instances",
7709
8268
  tags: ["trials"],
7710
8269
  responses: { "200": { description: "Trial instance list", content: { "application/json": { schema: ListEnvelope(TrialInstanceSchema) } } } },
7711
- "x-revturbine-operation": { exposure: "internal", resource: "trial-instances", persistence: { table: "trialInstances", mode: "list" } }
8270
+ // One row per trial EPISODE (BL-0247 / plan 276 TASK-12). This is the
8271
+ // ON CONFLICT target the Stripe binding writer upserts against, so a
8272
+ // re-delivered `customer.subscription.*` event updates the episode it
8273
+ // already created instead of minting a duplicate. NULL episode ids are
8274
+ // distinct in Postgres, so pre-existing and non-Stripe rows are unaffected.
8275
+ "x-revturbine-operation": { exposure: "internal", resource: "trial-instances", persistence: { table: "trialInstances", mode: "list", uniqueBy: ["tenant_id", "customer_id", "trial_episode_id"] } }
7712
8276
  })
7713
8277
  },
7714
8278
  "/api/trials/instances/{instanceId}": {
@@ -8715,6 +9279,16 @@ var PersonalizationTokenSchema = IdField.merge(TimestampFields).merge(TenantIdFi
8715
9279
  { id: "PersonalizationToken", "x-revturbine-schema-persistence": Persisted16, "x-revturbine-schema-exposure": Internal20, ...PENDING_PLAYBOOK_FACETS4, ...namedIdentity() }
8716
9280
  );
8717
9281
  var PersonalizationTokenAnchorSchema = makeAnchor("PersonalizationTokenAnchor");
9282
+ var ObjectiveSchema = IdField.merge(TimestampFields).merge(TenantIdField).merge(AnchorFields).merge(VersionFields).extend({
9283
+ anchor_id: z30.string().min(1).meta({ ...Unrestricted24, readOnly: true }),
9284
+ handle: HandleField.meta(Unrestricted24),
9285
+ name: NameField.meta(Unrestricted24),
9286
+ description: DescriptionField.meta(Unrestricted24),
9287
+ metadata: MetadataField.meta(Unrestricted24)
9288
+ }).meta(
9289
+ { id: "Objective", "x-revturbine-schema-persistence": Persisted16, "x-revturbine-schema-exposure": Internal20, ...PLAYBOOK_AUTHORING_FACETS2, ...namedIdentity() }
9290
+ );
9291
+ var ObjectiveAnchorSchema = makeAnchor("ObjectiveAnchor");
8718
9292
  var OnboardingStateSchema = z30.enum(["not_started", "started", "details_submitted", "charges_enabled", "activated", "deauthorized"]).meta({ id: "OnboardingState", "x-revturbine-schema-persistence": Transient24, "x-revturbine-schema-exposure": Internal20 });
8719
9293
  var StripeIntegrationConfigSchema = IdField.merge(TimestampFields).merge(TenantIdField).merge(AnchorFields).merge(VersionFields).extend({
8720
9294
  handle: HandleField.meta({ ...Unrestricted24, readOnly: true }),
@@ -9032,7 +9606,9 @@ var RevTurbineConfigEntitlementRulesItemSchema = z30.object({
9032
9606
  }).shape,
9033
9607
  current_usage: z30.number().default(0).meta(Unrestricted24),
9034
9608
  /** How usage is partitioned across the identity hierarchy. */
9035
- allocation: UsageAllocationSchema.optional().meta(Unrestricted24)
9609
+ allocation: UsageAllocationSchema.optional().meta(Unrestricted24),
9610
+ /** Optional business objective, by `objectives[].handle` (VAL-OBJ-01). Config-only. */
9611
+ objective: ObjectiveField.meta(Unrestricted24)
9036
9612
  }).meta(
9037
9613
  { id: "RevTurbineConfigEntitlementRulesItem", "x-revturbine-schema-persistence": Transient24, "x-revturbine-schema-exposure": External12, ...PLAYBOOK_SDK_FACETS8 }
9038
9614
  );
@@ -9128,6 +9704,13 @@ var RevTurbineConfigPersonalizationTokensItemSchema = z30.object({
9128
9704
  }).meta(
9129
9705
  { id: "RevTurbineConfigPersonalizationTokensItem", "x-revturbine-schema-persistence": Transient24, "x-revturbine-schema-exposure": External12, ...PLAYBOOK_SDK_FACETS8 }
9130
9706
  );
9707
+ var RevTurbineConfigObjectivesItemSchema = z30.object({
9708
+ handle: HandleField.meta(Unrestricted24),
9709
+ name: NameField.meta(Unrestricted24),
9710
+ description: DescriptionField.meta(Unrestricted24)
9711
+ }).meta(
9712
+ { id: "RevTurbineConfigObjectivesItem", "x-revturbine-schema-persistence": Transient24, "x-revturbine-schema-exposure": External12, ...PLAYBOOK_AUTHORING_FACETS2 }
9713
+ );
9131
9714
  var MessageBlockContentSchema = z30.object({
9132
9715
  header: z30.string().optional().meta(Unrestricted24),
9133
9716
  body: z30.string().optional().meta(Unrestricted24),
@@ -9231,7 +9814,9 @@ var RevTurbineConfigPlacementItemSchema = z30.object({
9231
9814
  category: RevTurbineConfigPlacementCategorySchema.meta(Unrestricted24),
9232
9815
  trigger: RevTurbineConfigPlacementTriggerSchema.meta(Unrestricted24),
9233
9816
  payloads: z30.array(RevTurbineConfigStudioPayloadSchema).meta(Unrestricted24),
9234
- order: z30.number().int().min(0).meta(Unrestricted24)
9817
+ order: z30.number().int().min(0).meta(Unrestricted24),
9818
+ /** Optional business objective, by `objectives[].handle` (VAL-OBJ-01). Config-only. */
9819
+ objective: ObjectiveField.meta(Unrestricted24)
9235
9820
  }).meta(
9236
9821
  { id: "RevTurbineConfigPlacementItem", "x-revturbine-schema-persistence": Transient24, "x-revturbine-schema-exposure": External12, ...PLAYBOOK_SDK_FACETS8 }
9237
9822
  );
@@ -9325,6 +9910,14 @@ var PlaybookBodySchema = z30.object({
9325
9910
  */
9326
9911
  free_trial_rules: z30.array(RevTurbineConfigFreeTrialRuleItemSchema).optional().meta({ ...Unrestricted24, ...PLAYBOOK_AUTHORING_FACETS2 }),
9327
9912
  reverse_trial_rules: z30.array(RevTurbineConfigReverseTrialRuleItemSchema).optional().meta({ ...Unrestricted24, ...PLAYBOOK_AUTHORING_FACETS2 }),
9913
+ /**
9914
+ * Business objectives (BL-0176 / BL-0178, ruling D-15). Optional so every
9915
+ * Playbook authored before objectives existed still parses. Placements and
9916
+ * entitlement rules reference an entry by `handle` through their optional
9917
+ * `objective` field. Authoring-only: no runtime decision reads it, so it is
9918
+ * not an SDK input and never lowers into the IR.
9919
+ */
9920
+ objectives: z30.array(RevTurbineConfigObjectivesItemSchema).optional().meta({ ...Unrestricted24, ...PLAYBOOK_AUTHORING_FACETS2 }),
9328
9921
  // Plan / add-on variation prices carried by handle (plan 118 TASK-16). These
9329
9922
  // live on the legacy schema (not just the canonical Playbook body) so that a
9330
9923
  // legacy `version`-shaped config — the shape the demo-data configs and the
@@ -9591,6 +10184,56 @@ var configPaths = {
9591
10184
  "x-revturbine-operation": { exposure: "internal", resource: "personalization-tokens", persistence: { table: "personalizationTokenVersions", mode: "delete" } }
9592
10185
  })
9593
10186
  },
10187
+ "/api/objective-anchors": {
10188
+ get: operation({
10189
+ operationId: "listObjectiveAnchors",
10190
+ requestParams: { query: ListQueryParamsSchema },
10191
+ summary: "List objective anchors (identity registry)",
10192
+ tags: ["config"],
10193
+ responses: {
10194
+ "200": { description: "Objective anchor list", content: { "application/json": { schema: ListEnvelope(ObjectiveAnchorSchema) } } },
10195
+ default: { description: "Error response", content: { "application/json": { schema: ErrorEnvelope } } }
10196
+ },
10197
+ "x-revturbine-operation": { exposure: "internal", resource: "objective-anchors", persistence: { table: "objectives", mode: "list" } }
10198
+ })
10199
+ },
10200
+ "/api/config/objectives": {
10201
+ get: operation({
10202
+ operationId: "listObjectives",
10203
+ requestParams: { query: ListQueryParamsSchema },
10204
+ summary: "List objectives",
10205
+ tags: ["config"],
10206
+ responses: { "200": { description: "Objective list", content: { "application/json": { schema: ListEnvelope(ObjectiveSchema) } } } },
10207
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "list" } }
10208
+ }),
10209
+ post: operation({
10210
+ operationId: "createObjective",
10211
+ summary: "Create objective",
10212
+ tags: ["config"],
10213
+ requestBody: { required: true, content: { "application/json": { schema: toCreateSchema(ObjectiveSchema) } } },
10214
+ responses: { "201": { description: "Created", content: { "application/json": { schema: ObjectiveSchema } } } },
10215
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "create" } }
10216
+ })
10217
+ },
10218
+ "/api/config/objectives/{id}": {
10219
+ patch: operation({
10220
+ operationId: "updateObjective",
10221
+ requestParams: { path: z30.object({ id: z30.string() }) },
10222
+ summary: "Update objective",
10223
+ tags: ["config"],
10224
+ requestBody: { required: true, content: { "application/json": { schema: ObjectiveSchema.partial() } } },
10225
+ responses: { "200": { description: "Updated", content: { "application/json": { schema: ObjectiveSchema } } } },
10226
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "update" } }
10227
+ }),
10228
+ delete: operation({
10229
+ operationId: "deleteObjective",
10230
+ requestParams: { path: z30.object({ id: z30.string() }) },
10231
+ summary: "Delete objective",
10232
+ tags: ["config"],
10233
+ responses: { "204": { description: "Deleted" } },
10234
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "delete" } }
10235
+ })
10236
+ },
9594
10237
  "/api/config/stripe": {
9595
10238
  get: operation({
9596
10239
  operationId: "getStripeIntegrationConfig",
@@ -10876,6 +11519,19 @@ var CATALOG = {
10876
11519
  message: "This limit rule sets no enforcement \u2014 at the cap it blocks by default. Set enforcement explicitly (hard_block, block_with_upsell, degrade, allow_overage) if that isn't the intent.",
10877
11520
  specRef: "config-validation.md \xA75.8 (promoted \u2014 plan 179 Q-6)"
10878
11521
  },
11522
+ // handle uniqueness (BL-0325; spec §5.1 VAL-UNI-01). A handle duplicates
11523
+ // another object's handle within the same handle-bearing collection — the
11524
+ // structural counterpart to the DB's partial unique indexes (migration
11525
+ // 0050) and Compile+Activate's own handle collapse. `error_draft`: per §2
11526
+ // this is a malformed/structurally-invalid value ("a duplicate handle"),
11527
+ // so it must never enter the draft at all, on any surface — including an
11528
+ // external config at ingestion.
11529
+ "VAL-UNI-01": {
11530
+ id: "VAL-UNI-01",
11531
+ severity: "error_draft",
11532
+ message: "This handle is already in use.",
11533
+ specRef: "config-validation.md \xA75.1 (VAL-UNI-01)"
11534
+ },
10879
11535
  // segment → experiment reference (plan 183). Handle, not id.
10880
11536
  "VAL-SEG-01": {
10881
11537
  id: "VAL-SEG-01",
@@ -10883,6 +11539,15 @@ var CATALOG = {
10883
11539
  message: "This segment enrols users into an experiment that does not exist.",
10884
11540
  specRef: "config-validation.md \xA75.3 (plan 183)"
10885
11541
  },
11542
+ // reserved built-in segment handle prefix (plan 279 TASK-2). Built-in
11543
+ // `rt.<dimension>.<value>` definitions are generated at export; an authored
11544
+ // one collides with or impersonates a built-in.
11545
+ "VAL-SEG-02": {
11546
+ id: "VAL-SEG-02",
11547
+ severity: "error_launch",
11548
+ message: "This segment's handle starts with 'rt.', which is reserved for built-in segments.",
11549
+ specRef: "config-validation.md \xA75.4; targeting-studio-ui.md \xA74.1 (plan 279)"
11550
+ },
10886
11551
  // experiment metric → Semantic Catalog reference (plan 199). The identifier
10887
11552
  // is structurally valid, but the active catalog does not declare it.
10888
11553
  "VAL-EXP-01": {
@@ -10970,6 +11635,15 @@ var CATALOG = {
10970
11635
  message: "This entitlement rule links an entitlement that does not exist.",
10971
11636
  specRef: "config-validation.md \xA75.3 (BL-0164)"
10972
11637
  },
11638
+ // placement / entitlement rule → objective (BL-0176, ruling D-15): the
11639
+ // optional `objective` field references an `objectives[]` entity by handle.
11640
+ // Same §5.9 dangling-reference family and severity.
11641
+ "VAL-OBJ-01": {
11642
+ id: "VAL-OBJ-01",
11643
+ severity: "error_launch",
11644
+ message: "This placement or entitlement rule names an objective that does not exist.",
11645
+ specRef: "config-validation.md \xA75.9 (BL-0176)"
11646
+ },
10973
11647
  // BL-0168: VAL-PLC-07's `plansKnown` precondition (every plan row carries a
10974
11648
  // non-empty `unique_handle`) fails silently for a DB-shape plan row that
10975
11649
  // spells the identifier `handle` — the whole check goes dark for the
@@ -11010,6 +11684,168 @@ function categoryBucket(category) {
11010
11684
  if (normalized.includes("retention") || normalized.includes("winback") || normalized.includes("churn")) return 4;
11011
11685
  return 99;
11012
11686
  }
11687
+ var HANDLE_PATTERN2 = /^[a-z0-9._]{1,100}$/;
11688
+ var BUILTIN_SEGMENT_HANDLE_PREFIX = "rt.";
11689
+ var BUILTIN_TRAIT_KEY_PREFIX = "rt_";
11690
+ var MAX_HANDLE_LENGTH = 100;
11691
+ function dimension(key2, name, legacyAbbr, values, opts = {}) {
11692
+ const legacyPrefix = `seg:${legacyAbbr}:`;
11693
+ return {
11694
+ key: key2,
11695
+ name,
11696
+ traitKey: `${BUILTIN_TRAIT_KEY_PREFIX}${key2}`,
11697
+ dimensionId: `${BUILTIN_SEGMENT_HANDLE_PREFIX}${key2}`,
11698
+ vocabulary: opts.vocabulary ?? "closed",
11699
+ values: values.map(([value, valueName, legacyValue]) => ({
11700
+ value,
11701
+ name: valueName,
11702
+ legacyPickerId: `${legacyPrefix}${legacyValue ?? value}`
11703
+ })),
11704
+ ...opts.fallback !== void 0 ? { fallback: opts.fallback } : {},
11705
+ legacyPrefix
11706
+ };
11707
+ }
11708
+ var BUILTIN_DIMENSIONS = [
11709
+ dimension("registration_state", "Registration State", "reg", [
11710
+ ["unregistered", "Unregistered"],
11711
+ ["registered", "Registered"]
11712
+ ]),
11713
+ dimension(
11714
+ "activity_level",
11715
+ "Activity State",
11716
+ "act",
11717
+ [
11718
+ ["new", "New"],
11719
+ ["high", "High Activity"],
11720
+ ["medium", "Medium Activity"],
11721
+ ["low", "Low Activity"],
11722
+ ["inactive", "Inactive"]
11723
+ ],
11724
+ { fallback: "new" }
11725
+ ),
11726
+ dimension(
11727
+ "subscription_state",
11728
+ "Subscription State",
11729
+ "sub",
11730
+ [
11731
+ ["none", "None"],
11732
+ ["trial", "Trial"],
11733
+ ["paid", "Paid"],
11734
+ ["cancelled", "Cancelled"]
11735
+ ],
11736
+ { fallback: "none" }
11737
+ ),
11738
+ dimension(
11739
+ "trial_type",
11740
+ "Trial Type",
11741
+ "trial",
11742
+ [
11743
+ ["none", "Not in trial"],
11744
+ ["free_trial", "Free trial", "free"],
11745
+ ["reverse_trial", "Reverse trial", "reverse"]
11746
+ ],
11747
+ { fallback: "none" }
11748
+ ),
11749
+ dimension("seat_type", "Seat Type", "seat", [], { vocabulary: "seat_type" }),
11750
+ dimension("buyer_role", "Buyer Role", "role", [
11751
+ ["buyer", "Buyer"],
11752
+ ["non_buyer", "Non-buyer"]
11753
+ ]),
11754
+ dimension(
11755
+ "email_type",
11756
+ "Email Type",
11757
+ "email",
11758
+ [
11759
+ ["business", "Business email"],
11760
+ ["personal", "Personal email"],
11761
+ ["unknown", "Unknown"]
11762
+ ],
11763
+ { fallback: "unknown" }
11764
+ ),
11765
+ dimension(
11766
+ "billing_health",
11767
+ "Billing Health",
11768
+ "bill",
11769
+ [
11770
+ ["no_billing", "No billing", "none"],
11771
+ ["good_standing", "Good standing", "good"],
11772
+ ["trial_payment_method_attached", "Trial \u2014 payment method attached", "trial_pm"],
11773
+ ["payment_method_missing", "Payment method missing", "pm_missing"],
11774
+ ["payment_failed", "Payment failed", "failed"],
11775
+ ["payment_overdue", "Payment overdue", "overdue"],
11776
+ ["cancelled", "Cancelled"]
11777
+ ],
11778
+ { fallback: "no_billing" }
11779
+ ),
11780
+ dimension("region", "Country / Region", "geo", [
11781
+ ["us_canada", "US / Canada"],
11782
+ ["europe", "Europe"],
11783
+ ["rest_of_world", "Rest of World", "row"]
11784
+ ]),
11785
+ dimension(
11786
+ "device_type",
11787
+ "Device Type",
11788
+ "dev",
11789
+ [
11790
+ ["desktop", "Desktop"],
11791
+ ["mobile", "Mobile"],
11792
+ ["tablet", "Tablet"],
11793
+ ["unknown", "Unknown"]
11794
+ ],
11795
+ { fallback: "unknown" }
11796
+ )
11797
+ ];
11798
+ var BY_KEY = new Map(
11799
+ BUILTIN_DIMENSIONS.map((d) => [d.key, d])
11800
+ );
11801
+ function isBuiltinDimensionKey(key2) {
11802
+ return BY_KEY.has(key2);
11803
+ }
11804
+ function getBuiltinDimension(key2) {
11805
+ const found = BY_KEY.get(key2);
11806
+ if (!found) throw new Error(`Unknown built-in dimension '${key2}'`);
11807
+ return found;
11808
+ }
11809
+ function builtinSegmentHandle(key2, value) {
11810
+ return `${BUILTIN_SEGMENT_HANDLE_PREFIX}${key2}.${value}`;
11811
+ }
11812
+ function isReservedSegmentHandle(handle) {
11813
+ return handle.startsWith(BUILTIN_SEGMENT_HANDLE_PREFIX);
11814
+ }
11815
+ function isBuiltinDimensionValue(key2, value) {
11816
+ const dim = getBuiltinDimension(key2);
11817
+ if (dim.vocabulary === "closed") return dim.values.some((v) => v.value === value);
11818
+ return HANDLE_PATTERN2.test(value) && builtinSegmentHandle(key2, value).length <= MAX_HANDLE_LENGTH;
11819
+ }
11820
+ function parseBuiltinSegmentHandle(handle) {
11821
+ if (!isReservedSegmentHandle(handle)) return void 0;
11822
+ const rest = handle.slice(BUILTIN_SEGMENT_HANDLE_PREFIX.length);
11823
+ const dot = rest.indexOf(".");
11824
+ if (dot <= 0) return void 0;
11825
+ const key2 = rest.slice(0, dot);
11826
+ const value = rest.slice(dot + 1);
11827
+ if (!isBuiltinDimensionKey(key2) || !isBuiltinDimensionValue(key2, value)) return void 0;
11828
+ return { key: key2, value };
11829
+ }
11830
+ function isGeneratedBuiltinSegment(segment) {
11831
+ if (typeof segment.handle !== "string") return false;
11832
+ const parsed = parseBuiltinSegmentHandle(segment.handle);
11833
+ if (!parsed) return false;
11834
+ const dim = getBuiltinDimension(parsed.key);
11835
+ if (segment.dimension_id !== dim.dimensionId) return false;
11836
+ if (segment.experiment_handle !== void 0 && segment.experiment_handle !== null) return false;
11837
+ const predicates = segment.predicates;
11838
+ if (!Array.isArray(predicates) || predicates.length !== 1) return false;
11839
+ const [p] = predicates;
11840
+ if (!p || typeof p !== "object") return false;
11841
+ const { field, operator, value } = p;
11842
+ return field === dim.traitKey && operator === "eq" && value === parsed.value;
11843
+ }
11844
+ var LEGACY_BY_ID = new Map(
11845
+ BUILTIN_DIMENSIONS.flatMap(
11846
+ (d) => d.values.map((v) => [v.legacyPickerId, builtinSegmentHandle(d.key, v.value)])
11847
+ )
11848
+ );
11013
11849
  function runSemanticRules(graph, opts = {}) {
11014
11850
  return [
11015
11851
  ...checkRuleOverlaps(graph),
@@ -11019,6 +11855,8 @@ function runSemanticRules(graph, opts = {}) {
11019
11855
  ...checkPlacementAuthoring(graph),
11020
11856
  ...checkTrialRuleWidestAudience(graph),
11021
11857
  ...checkSegmentExperimentRefs(graph),
11858
+ ...checkReservedSegmentHandles(graph),
11859
+ ...checkHandleUniqueness(graph),
11022
11860
  ...checkExperimentMetricRefs(graph, opts.knownMetricIds),
11023
11861
  ...checkDanglingReferences(graph)
11024
11862
  ];
@@ -11054,9 +11892,43 @@ function checkDanglingReferences(graph) {
11054
11892
  ...checkTrialPlanRefs(graph),
11055
11893
  ...checkEntitlementRuleTargetRefs(graph),
11056
11894
  ...checkEntitlementRuleEntitlementRefs(graph),
11057
- ...checkPayloadSegmentChipRefs(graph)
11895
+ ...checkPayloadSegmentChipRefs(graph),
11896
+ ...checkObjectiveRefs(graph)
11058
11897
  ];
11059
11898
  }
11899
+ var OBJECTIVE_REFERRERS = [
11900
+ { table: "placements", objectType: "placement", label: "Placement", studio: "placements" },
11901
+ { table: "entitlement_rules", objectType: "entitlement_rule", label: "Entitlement rule", studio: "plans-entitlements" }
11902
+ ];
11903
+ function checkObjectiveRefs(graph) {
11904
+ const known = referenceNamespace(graph, "objectives", "handle");
11905
+ if (!known) return [];
11906
+ const findings = [];
11907
+ for (const referrer of OBJECTIVE_REFERRERS) {
11908
+ for (const [index, row] of (graph[referrer.table] ?? []).entries()) {
11909
+ const value = row.objective;
11910
+ if (typeof value !== "string" || value.length === 0 || known.has(value)) continue;
11911
+ const id = String(row.handle ?? row.id ?? "");
11912
+ findings.push(
11913
+ finding(
11914
+ "VAL-OBJ-01",
11915
+ {
11916
+ object_type: referrer.objectType,
11917
+ object_id: id,
11918
+ field: "objective",
11919
+ path: [referrer.table, index, "objective"],
11920
+ studio: referrer.studio
11921
+ },
11922
+ {
11923
+ message: `${referrer.label} '${id || "unnamed"}' names objective '${value}', which no objective declares.`,
11924
+ detail: "An objective reference must be an objective's handle, not its config id. A reference that matches nothing drops this object out of the objective slice, so its results report under no objective." + knownHandlesHint(known, "objective handles")
11925
+ }
11926
+ )
11927
+ );
11928
+ }
11929
+ }
11930
+ return findings;
11931
+ }
11060
11932
  function checkTriggerEntitlementRefs(graph) {
11061
11933
  const known = referenceNamespace(graph, "entitlements", "unique_handle");
11062
11934
  if (!known) return [];
@@ -11440,6 +12312,90 @@ function checkSegmentExperimentRefs(graph) {
11440
12312
  }
11441
12313
  return findings;
11442
12314
  }
12315
+ function checkReservedSegmentHandles(graph) {
12316
+ const findings = [];
12317
+ for (const table2 of ["segments", "segment_values"]) {
12318
+ for (const [index, segment] of (graph[table2] ?? []).entries()) {
12319
+ const handle = segment.handle;
12320
+ if (typeof handle !== "string" || !isReservedSegmentHandle(handle)) continue;
12321
+ if (table2 === "segments" && isGeneratedBuiltinSegment(segment)) continue;
12322
+ findings.push(
12323
+ finding(
12324
+ "VAL-SEG-02",
12325
+ {
12326
+ object_type: "segment",
12327
+ object_id: handle,
12328
+ field: "handle",
12329
+ path: [table2, index, "handle"],
12330
+ studio: "targeting"
12331
+ },
12332
+ {
12333
+ message: `Segment '${handle}' uses the reserved '${BUILTIN_SEGMENT_HANDLE_PREFIX}' handle prefix.`,
12334
+ detail: `Handles starting with '${BUILTIN_SEGMENT_HANDLE_PREFIX}' are reserved for RevTurbine's built-in segments, which are generated at export. Rename the segment; to target a built-in value, select it from the built-in dimension instead.`
12335
+ }
12336
+ )
12337
+ );
12338
+ }
12339
+ }
12340
+ return findings;
12341
+ }
12342
+ var HANDLE_UNIQUENESS_COLLECTIONS = [
12343
+ { table: "plans", handleFields: ["unique_handle", "handle"], objectType: "plan", studio: "plans-entitlements" },
12344
+ { table: "addons", handleFields: ["unique_handle", "handle"], objectType: "addon", studio: "plans-entitlements" },
12345
+ {
12346
+ table: "entitlements",
12347
+ handleFields: ["unique_handle", "handle"],
12348
+ objectType: "entitlement",
12349
+ studio: "plans-entitlements"
12350
+ },
12351
+ {
12352
+ table: "segments",
12353
+ handleFields: ["handle"],
12354
+ objectType: "segment",
12355
+ studio: "targeting",
12356
+ exempt: (row) => isGeneratedBuiltinSegment(row)
12357
+ },
12358
+ { table: "segment_values", handleFields: ["handle"], objectType: "segment", studio: "targeting" },
12359
+ { table: "seat_types", handleFields: ["handle"], objectType: "seat_type", studio: "plans-entitlements" }
12360
+ ];
12361
+ function checkHandleUniqueness(graph) {
12362
+ const findings = [];
12363
+ for (const { table: table2, handleFields, objectType, studio, exempt } of HANDLE_UNIQUENESS_COLLECTIONS) {
12364
+ const rows = graph[table2];
12365
+ if (!rows?.length) continue;
12366
+ const seen = /* @__PURE__ */ new Set();
12367
+ for (const [index, row] of rows.entries()) {
12368
+ if (row.is_current === false) continue;
12369
+ if (exempt?.(row)) continue;
12370
+ const field = firstStringField(row, handleFields);
12371
+ if (!field) continue;
12372
+ const tenant = String(row.tenant_id ?? "");
12373
+ const environment = String(row.environment_id ?? "");
12374
+ const key2 = `${tenant}::${environment}::${field.value}`;
12375
+ if (!seen.has(key2)) {
12376
+ seen.add(key2);
12377
+ continue;
12378
+ }
12379
+ const id = String(row.id ?? field.value);
12380
+ findings.push(
12381
+ finding(
12382
+ "VAL-UNI-01",
12383
+ {
12384
+ object_type: objectType,
12385
+ object_id: id,
12386
+ field: field.field,
12387
+ path: [table2, index, field.field],
12388
+ studio
12389
+ },
12390
+ {
12391
+ detail: `Handle '${field.value}' is used by more than one ${objectType} \u2014 handles must be unique within a ${objectType} collection.`
12392
+ }
12393
+ )
12394
+ );
12395
+ }
12396
+ }
12397
+ return findings;
12398
+ }
11443
12399
  function checkExperimentMetricRefs(graph, knownMetricIds) {
11444
12400
  if (!knownMetricIds) return [];
11445
12401
  const findings = [];
@@ -11688,7 +12644,6 @@ function evaluate(graph, opts = {}) {
11688
12644
  if (!opts.focus) return findings;
11689
12645
  return findings.map((f) => spotlights(f, opts.focus) ? { ...f, spotlight: true } : f);
11690
12646
  }
11691
- var HANDLE_PATTERN2 = /^[a-z0-9._]{1,100}$/;
11692
12647
  var SchemaPersistence2 = {
11693
12648
  Persisted: "persisted",
11694
12649
  Transient: "transient"
@@ -11951,6 +12906,7 @@ var CtaActionTypeSchema2 = z39.enum([
11951
12906
  "custom"
11952
12907
  ]).meta({ id: "CtaActionType", "x-revturbine-schema-persistence": Transient30, "x-revturbine-schema-exposure": External14 });
11953
12908
  var ObjectiveField2 = HandleField2.optional();
12909
+ var NullableObjectiveField2 = z39.string().min(1).max(100).nullable().optional();
11954
12910
  var IdentityKind2 = {
11955
12911
  /** Author-given, human-meaningful handle (plans, entitlements, segments, …). */
11956
12912
  Named: "named",
@@ -12158,10 +13114,14 @@ var EntitlementRuleSchema2 = IdField2.merge(TimestampFields2).merge(TenantIdFiel
12158
13114
  visibility: RuleVisibilitySchema2.default("public").meta(Unrestricted210),
12159
13115
  // Business objective this rule monetizes for — the analytics `objective`
12160
13116
  // slice, which resolves `rule_handle → objective` through a configuration
12161
- // snapshot (BL-0065 / worksheet G4). Config-only, and deliberately NOT part
12162
- // of the minted identity below: an objective relabel is a behaviour-only
12163
- // edit that coalesces onto the same rule, not a new rule scope.
12164
- objective: ObjectiveField2.meta(Unrestricted210),
13117
+ // snapshot (BL-0065 / worksheet G4). A reference by handle to an
13118
+ // `objectives[]` entity (D-15; VAL-OBJ-01). Config-only, and deliberately
13119
+ // NOT part of the minted identity below: an objective relabel is a
13120
+ // behaviour-only edit that coalesces onto the same rule, not a new rule scope.
13121
+ // Nullable on the persisted entity so an editor can CLEAR the reference
13122
+ // (a PATCH merges, so omission keeps the old value); the portable
13123
+ // projection stays optional-only and omits it when unset.
13124
+ objective: NullableObjectiveField2.meta(Unrestricted210),
12165
13125
  // Usage-Limit "measured over" window, rule-level (plan #55). Rate Limit
12166
13126
  // keeps its entitlement-level `period_scope`; this is the per-rule one.
12167
13127
  period_scope: UsagePeriodScopeSchema2.optional().meta(Unrestricted210),
@@ -12580,6 +13540,39 @@ var TrialInstanceSchema2 = IdField2.merge(TimestampFields2).merge(TenantIdField2
12580
13540
  usage_limit_value: z72.number().int().min(1).optional().meta(Unrestricted33),
12581
13541
  converted_at: NullableDatetimeField2.meta(Unrestricted33),
12582
13542
  cancelled_at: NullableDatetimeField2.meta(Unrestricted33),
13543
+ /**
13544
+ * Stable identity of the trial EPISODE this row represents
13545
+ * (BL-0247 / plan 276 TASK-12). A customer can run more than one
13546
+ * trial, and a subscription can re-enter `trialing`, so the
13547
+ * (customer, subscription) pair does not name an occurrence. The
13548
+ * billing lane already discriminates an episode by the Stripe
13549
+ * subscription id plus its `trial_start`; this column persists
13550
+ * that same discriminator so a fact about one episode can be
13551
+ * written to exactly that episode's row.
13552
+ *
13553
+ * Nullable: rows created before the column existed, and rows
13554
+ * created by paths that have no Stripe evidence to mint an id
13555
+ * from, carry NULL. NULL means "this episode has no first-class
13556
+ * identity", never "episode zero" — and Postgres treats NULLs as
13557
+ * distinct, so the composite UNIQUE below does not collapse them.
13558
+ */
13559
+ trial_episode_id: z72.string().min(1).nullable().optional().meta(Unrestricted33),
13560
+ /**
13561
+ * The Stripe subscription this episode belongs to — the binding
13562
+ * that lets a Stripe fact resolve one episode without guessing
13563
+ * (BL-0245 carried this in `metadata.stripe_subscription_id`).
13564
+ * Nullable because a trial need not originate from Stripe at all.
13565
+ */
13566
+ stripe_subscription_id: z72.string().min(1).nullable().optional().meta(Unrestricted33),
13567
+ /**
13568
+ * The EVIDENCED end of the episode: the moment a provider fact
13569
+ * said the trial actually stopped. Distinct from `expires_at`,
13570
+ * which is the SCHEDULED end and must stay distinguishable
13571
+ * (plan 276 R-1 — no clock authority; an elapsed `expires_at`
13572
+ * with no fact behind it is not an end). Null until a fact
13573
+ * proves one.
13574
+ */
13575
+ actual_end_at: NullableDatetimeField2.meta(Unrestricted33),
12583
13576
  metadata: MetadataField2.meta(Unrestricted33)
12584
13577
  }).meta(
12585
13578
  { id: "TrialInstance", "x-revturbine-schema-persistence": Persisted32, "x-revturbine-schema-exposure": Internal32 }
@@ -12782,7 +13775,12 @@ var trialPaths2 = {
12782
13775
  summary: "List trial instances",
12783
13776
  tags: ["trials"],
12784
13777
  responses: { "200": { description: "Trial instance list", content: { "application/json": { schema: ListEnvelope2(TrialInstanceSchema2) } } } },
12785
- "x-revturbine-operation": { exposure: "internal", resource: "trial-instances", persistence: { table: "trialInstances", mode: "list" } }
13778
+ // One row per trial EPISODE (BL-0247 / plan 276 TASK-12). This is the
13779
+ // ON CONFLICT target the Stripe binding writer upserts against, so a
13780
+ // re-delivered `customer.subscription.*` event updates the episode it
13781
+ // already created instead of minting a duplicate. NULL episode ids are
13782
+ // distinct in Postgres, so pre-existing and non-Stripe rows are unaffected.
13783
+ "x-revturbine-operation": { exposure: "internal", resource: "trial-instances", persistence: { table: "trialInstances", mode: "list", uniqueBy: ["tenant_id", "customer_id", "trial_episode_id"] } }
12786
13784
  })
12787
13785
  },
12788
13786
  "/api/trials/instances/{instanceId}": {
@@ -13336,6 +14334,16 @@ var PersonalizationTokenSchema2 = IdField2.merge(TimestampFields2).merge(TenantI
13336
14334
  { id: "PersonalizationToken", "x-revturbine-schema-persistence": Persisted52, "x-revturbine-schema-exposure": Internal42, ...PENDING_PLAYBOOK_FACETS22, ...namedIdentity2() }
13337
14335
  );
13338
14336
  var PersonalizationTokenAnchorSchema2 = makeAnchor2("PersonalizationTokenAnchor");
14337
+ var ObjectiveSchema2 = IdField2.merge(TimestampFields2).merge(TenantIdField2).merge(AnchorFields2).merge(VersionFields2).extend({
14338
+ anchor_id: z92.string().min(1).meta({ ...Unrestricted52, readOnly: true }),
14339
+ handle: HandleField2.meta(Unrestricted52),
14340
+ name: NameField2.meta(Unrestricted52),
14341
+ description: DescriptionField2.meta(Unrestricted52),
14342
+ metadata: MetadataField2.meta(Unrestricted52)
14343
+ }).meta(
14344
+ { id: "Objective", "x-revturbine-schema-persistence": Persisted52, "x-revturbine-schema-exposure": Internal42, ...PLAYBOOK_AUTHORING_FACETS22, ...namedIdentity2() }
14345
+ );
14346
+ var ObjectiveAnchorSchema2 = makeAnchor2("ObjectiveAnchor");
13339
14347
  var OnboardingStateSchema2 = z92.enum(["not_started", "started", "details_submitted", "charges_enabled", "activated", "deauthorized"]).meta({ id: "OnboardingState", "x-revturbine-schema-persistence": Transient52, "x-revturbine-schema-exposure": Internal42 });
13340
14348
  var StripeIntegrationConfigSchema2 = IdField2.merge(TimestampFields2).merge(TenantIdField2).merge(AnchorFields2).merge(VersionFields2).extend({
13341
14349
  handle: HandleField2.meta({ ...Unrestricted52, readOnly: true }),
@@ -13653,7 +14661,9 @@ var RevTurbineConfigEntitlementRulesItemSchema2 = z92.object({
13653
14661
  }).shape,
13654
14662
  current_usage: z92.number().default(0).meta(Unrestricted52),
13655
14663
  /** How usage is partitioned across the identity hierarchy. */
13656
- allocation: UsageAllocationSchema2.optional().meta(Unrestricted52)
14664
+ allocation: UsageAllocationSchema2.optional().meta(Unrestricted52),
14665
+ /** Optional business objective, by `objectives[].handle` (VAL-OBJ-01). Config-only. */
14666
+ objective: ObjectiveField2.meta(Unrestricted52)
13657
14667
  }).meta(
13658
14668
  { id: "RevTurbineConfigEntitlementRulesItem", "x-revturbine-schema-persistence": Transient52, "x-revturbine-schema-exposure": External42, ...PLAYBOOK_SDK_FACETS42 }
13659
14669
  );
@@ -13749,6 +14759,13 @@ var RevTurbineConfigPersonalizationTokensItemSchema2 = z92.object({
13749
14759
  }).meta(
13750
14760
  { id: "RevTurbineConfigPersonalizationTokensItem", "x-revturbine-schema-persistence": Transient52, "x-revturbine-schema-exposure": External42, ...PLAYBOOK_SDK_FACETS42 }
13751
14761
  );
14762
+ var RevTurbineConfigObjectivesItemSchema2 = z92.object({
14763
+ handle: HandleField2.meta(Unrestricted52),
14764
+ name: NameField2.meta(Unrestricted52),
14765
+ description: DescriptionField2.meta(Unrestricted52)
14766
+ }).meta(
14767
+ { id: "RevTurbineConfigObjectivesItem", "x-revturbine-schema-persistence": Transient52, "x-revturbine-schema-exposure": External42, ...PLAYBOOK_AUTHORING_FACETS22 }
14768
+ );
13752
14769
  var MessageBlockContentSchema2 = z92.object({
13753
14770
  header: z92.string().optional().meta(Unrestricted52),
13754
14771
  body: z92.string().optional().meta(Unrestricted52),
@@ -13852,7 +14869,9 @@ var RevTurbineConfigPlacementItemSchema2 = z92.object({
13852
14869
  category: RevTurbineConfigPlacementCategorySchema2.meta(Unrestricted52),
13853
14870
  trigger: RevTurbineConfigPlacementTriggerSchema2.meta(Unrestricted52),
13854
14871
  payloads: z92.array(RevTurbineConfigStudioPayloadSchema2).meta(Unrestricted52),
13855
- order: z92.number().int().min(0).meta(Unrestricted52)
14872
+ order: z92.number().int().min(0).meta(Unrestricted52),
14873
+ /** Optional business objective, by `objectives[].handle` (VAL-OBJ-01). Config-only. */
14874
+ objective: ObjectiveField2.meta(Unrestricted52)
13856
14875
  }).meta(
13857
14876
  { id: "RevTurbineConfigPlacementItem", "x-revturbine-schema-persistence": Transient52, "x-revturbine-schema-exposure": External42, ...PLAYBOOK_SDK_FACETS42 }
13858
14877
  );
@@ -13946,6 +14965,14 @@ var PlaybookBodySchema2 = z92.object({
13946
14965
  */
13947
14966
  free_trial_rules: z92.array(RevTurbineConfigFreeTrialRuleItemSchema2).optional().meta({ ...Unrestricted52, ...PLAYBOOK_AUTHORING_FACETS22 }),
13948
14967
  reverse_trial_rules: z92.array(RevTurbineConfigReverseTrialRuleItemSchema2).optional().meta({ ...Unrestricted52, ...PLAYBOOK_AUTHORING_FACETS22 }),
14968
+ /**
14969
+ * Business objectives (BL-0176 / BL-0178, ruling D-15). Optional so every
14970
+ * Playbook authored before objectives existed still parses. Placements and
14971
+ * entitlement rules reference an entry by `handle` through their optional
14972
+ * `objective` field. Authoring-only: no runtime decision reads it, so it is
14973
+ * not an SDK input and never lowers into the IR.
14974
+ */
14975
+ objectives: z92.array(RevTurbineConfigObjectivesItemSchema2).optional().meta({ ...Unrestricted52, ...PLAYBOOK_AUTHORING_FACETS22 }),
13949
14976
  // Plan / add-on variation prices carried by handle (plan 118 TASK-16). These
13950
14977
  // live on the legacy schema (not just the canonical Playbook body) so that a
13951
14978
  // legacy `version`-shaped config — the shape the demo-data configs and the
@@ -14211,6 +15238,56 @@ var configPaths2 = {
14211
15238
  "x-revturbine-operation": { exposure: "internal", resource: "personalization-tokens", persistence: { table: "personalizationTokenVersions", mode: "delete" } }
14212
15239
  })
14213
15240
  },
15241
+ "/api/objective-anchors": {
15242
+ get: operation2({
15243
+ operationId: "listObjectiveAnchors",
15244
+ requestParams: { query: ListQueryParamsSchema2 },
15245
+ summary: "List objective anchors (identity registry)",
15246
+ tags: ["config"],
15247
+ responses: {
15248
+ "200": { description: "Objective anchor list", content: { "application/json": { schema: ListEnvelope2(ObjectiveAnchorSchema2) } } },
15249
+ default: { description: "Error response", content: { "application/json": { schema: ErrorEnvelope2 } } }
15250
+ },
15251
+ "x-revturbine-operation": { exposure: "internal", resource: "objective-anchors", persistence: { table: "objectives", mode: "list" } }
15252
+ })
15253
+ },
15254
+ "/api/config/objectives": {
15255
+ get: operation2({
15256
+ operationId: "listObjectives",
15257
+ requestParams: { query: ListQueryParamsSchema2 },
15258
+ summary: "List objectives",
15259
+ tags: ["config"],
15260
+ responses: { "200": { description: "Objective list", content: { "application/json": { schema: ListEnvelope2(ObjectiveSchema2) } } } },
15261
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "list" } }
15262
+ }),
15263
+ post: operation2({
15264
+ operationId: "createObjective",
15265
+ summary: "Create objective",
15266
+ tags: ["config"],
15267
+ requestBody: { required: true, content: { "application/json": { schema: toCreateSchema2(ObjectiveSchema2) } } },
15268
+ responses: { "201": { description: "Created", content: { "application/json": { schema: ObjectiveSchema2 } } } },
15269
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "create" } }
15270
+ })
15271
+ },
15272
+ "/api/config/objectives/{id}": {
15273
+ patch: operation2({
15274
+ operationId: "updateObjective",
15275
+ requestParams: { path: z92.object({ id: z92.string() }) },
15276
+ summary: "Update objective",
15277
+ tags: ["config"],
15278
+ requestBody: { required: true, content: { "application/json": { schema: ObjectiveSchema2.partial() } } },
15279
+ responses: { "200": { description: "Updated", content: { "application/json": { schema: ObjectiveSchema2 } } } },
15280
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "update" } }
15281
+ }),
15282
+ delete: operation2({
15283
+ operationId: "deleteObjective",
15284
+ requestParams: { path: z92.object({ id: z92.string() }) },
15285
+ summary: "Delete objective",
15286
+ tags: ["config"],
15287
+ responses: { "204": { description: "Deleted" } },
15288
+ "x-revturbine-operation": { exposure: "internal", resource: "objectives", persistence: { table: "objectiveVersions", mode: "delete" } }
15289
+ })
15290
+ },
14214
15291
  "/api/config/stripe": {
14215
15292
  get: operation2({
14216
15293
  operationId: "getStripeIntegrationConfig",
@@ -14385,7 +15462,7 @@ function repairDeprecatedFields(config, schema = PlaybookObjectSchema2) {
14385
15462
  }
14386
15463
 
14387
15464
  // src/schema/version.ts
14388
- var SCHEMA_VERSION = "0.1.343";
15465
+ var SCHEMA_VERSION = "0.1.362";
14389
15466
 
14390
15467
  // src/lib/credentials.ts
14391
15468
  import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
@@ -14674,7 +15751,9 @@ var COLLECTIONS = [
14674
15751
  "surface_templates",
14675
15752
  "placements",
14676
15753
  "free_trial_rules",
14677
- "reverse_trial_rules"
15754
+ "reverse_trial_rules",
15755
+ // BL-0176 / BL-0178: first-class objectives, keyed by handle.
15756
+ "objectives"
14678
15757
  ];
14679
15758
  function keyOf(item) {
14680
15759
  return item?.handle ?? item?.unique_handle ?? item?.name ?? item?.id ?? JSON.stringify(item);