@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.
- package/dist/cli.js +1122 -43
- 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).
|
|
3005
|
-
//
|
|
3006
|
-
//
|
|
3007
|
-
|
|
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).
|
|
3364
|
-
//
|
|
3365
|
-
//
|
|
3366
|
-
//
|
|
3367
|
-
|
|
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
|
-
*
|
|
3946
|
+
* Personalization token map.
|
|
3910
3947
|
*
|
|
3911
3948
|
* Holds SDK-derived tokens (plan_name, usage_current, etc.) merged with
|
|
3912
|
-
* app-provided tokens
|
|
3913
|
-
*
|
|
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(
|
|
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
|
|
5082
|
-
* are NOT decorative: a concept that declares
|
|
5083
|
-
* has been mapped, and
|
|
5084
|
-
*
|
|
5085
|
-
*
|
|
5086
|
-
*
|
|
5087
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
12162
|
-
//
|
|
12163
|
-
//
|
|
12164
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|