@revturbine/cli 0.19.2 → 0.19.4
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/README.md +1 -1
- package/dist/cli.js +694 -45
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
80
80
|
| `status` | The live Release and the open draft, side by side. |
|
|
81
81
|
| `history` | The Release Version Log, newest first. |
|
|
82
82
|
| `preview` | The open draft's staged changes. |
|
|
83
|
-
| `evaluate` | Run a config version's placement/entitlement decisions locally for a user context. Use `--slot` with optional `--component-type`; deprecated `--surface-type` remains an alias. |
|
|
83
|
+
| `evaluate` | Run a config version's placement/entitlement decisions locally for a user context. Use `--slot` with optional `--component-type`; deprecated `--surface-type` remains an alias. The ctx file carries the whole decision context — `plan_handle`, `traits`, `usage`, `trial` (or `trial_instances` + `now_iso`) — and `--usage` / `--trial-status` / `--now` override those keys, so usage-threshold and trial placements decide here exactly as they do in an app. Output reports `payload_ids`: which authored payload the resolver chose per placement, plus the SDK's `personalization_tokens`. |
|
|
84
84
|
| `generate types` | Generate a TypeScript module of typed Playbook handles from any config version — `Entitlements` (namespaced by type, with the `EntitlementHandle` union for type-safe `can()`/`gate()`/`checkEntitlement()` call sites), plus `Plans`, `Segments`, `SurfaceTemplates`, and `UiPathActionTypes`. Const objects + literal-union types (erasable — no enums). `--out <path>` writes the file; the generated header records the exact command to regenerate it. `--json` for the raw handle map. |
|
|
85
85
|
| `analytics catalog\|templates\|views\|view\|create\|preview\|query` | Work with the hosted Semantic Catalog and canonical analytics-view documents. Create and preview pass the document through unchanged to the same server contract used by the web editor and MCP tools; preview remains subject to the server's query limits. |
|
|
86
86
|
| `ingest-keys create` (alias `mint`) | Mint a tenant-bound public ingest token for browser SDK use. Requires one or more `--origin` values; optional `--ip` restrictions. The full token is returned once. `ingest-keys list` shows ids/previews and `ingest-keys revoke <id>` invalidates one. |
|
package/dist/cli.js
CHANGED
|
@@ -2353,6 +2353,7 @@ var CtaActionTypeSchema = z2.enum([
|
|
|
2353
2353
|
"snooze",
|
|
2354
2354
|
"custom"
|
|
2355
2355
|
]).meta({ id: "CtaActionType", "x-revturbine-schema-persistence": Transient, "x-revturbine-schema-exposure": External });
|
|
2356
|
+
var ObjectiveField = HandleField.optional();
|
|
2356
2357
|
var SchemaContext = {
|
|
2357
2358
|
Playbook: "playbook",
|
|
2358
2359
|
Branding: "branding",
|
|
@@ -2998,6 +2999,12 @@ var EntitlementRuleSchema = IdField.merge(TimestampFields).merge(TenantIdField).
|
|
|
2998
2999
|
// "match all users" (replaces the legacy 'all' sentinel).
|
|
2999
3000
|
segment_ids: z6.array(z6.string()).default([]).meta(Unrestricted3),
|
|
3000
3001
|
visibility: RuleVisibilitySchema.default("public").meta(Unrestricted3),
|
|
3002
|
+
// Business objective this rule monetizes for — the analytics `objective`
|
|
3003
|
+
// slice, which resolves `rule_handle → objective` through a configuration
|
|
3004
|
+
// snapshot (BL-0065 / worksheet G4). Config-only, and deliberately NOT part
|
|
3005
|
+
// of the minted identity below: an objective relabel is a behaviour-only
|
|
3006
|
+
// edit that coalesces onto the same rule, not a new rule scope.
|
|
3007
|
+
objective: ObjectiveField.meta(Unrestricted3),
|
|
3001
3008
|
// Usage-Limit "measured over" window, rule-level (plan #55). Rate Limit
|
|
3002
3009
|
// keeps its entitlement-level `period_scope`; this is the per-rule one.
|
|
3003
3010
|
period_scope: UsagePeriodScopeSchema.optional().meta(Unrestricted3),
|
|
@@ -3351,6 +3358,13 @@ var PlacementSchema = IdField.merge(TimestampFields).merge(TenantIdField).merge(
|
|
|
3351
3358
|
handle: HandleField.meta(Unrestricted4),
|
|
3352
3359
|
description: DescriptionField.meta(Unrestricted4),
|
|
3353
3360
|
category: PlacementCategorySchema.meta(Unrestricted4),
|
|
3361
|
+
// Business objective this placement monetizes for — the analytics
|
|
3362
|
+
// `objective` slice, which resolves `placement_id → objective` through a
|
|
3363
|
+
// configuration snapshot (BL-0065 / worksheet G4). Config-only: nothing in
|
|
3364
|
+
// the runtime decision reads it, so it is absent from the portable
|
|
3365
|
+
// RevTurbineConfig projection and the compiled payload. Free handle; see
|
|
3366
|
+
// `ObjectiveField` for why the value set is not enumerated here.
|
|
3367
|
+
objective: ObjectiveField.meta(Unrestricted4),
|
|
3354
3368
|
drag_order_in_category: z7.number().int().default(0).meta(Unrestricted4),
|
|
3355
3369
|
// Trigger config (populated based on category)
|
|
3356
3370
|
surface_slot_id: z7.string().optional().meta(Unrestricted4),
|
|
@@ -5526,7 +5540,29 @@ var placementLifecycleBase = {
|
|
|
5526
5540
|
surface_slot_id: nstr(),
|
|
5527
5541
|
payload_id: nstr(),
|
|
5528
5542
|
decision_id: nstr(),
|
|
5529
|
-
decision_source: nstr()
|
|
5543
|
+
decision_source: nstr(),
|
|
5544
|
+
/**
|
|
5545
|
+
* The `unique_handle` of the entitlement rule / placement rule whose verdict
|
|
5546
|
+
* this event records; `null` when no rule matched (BL-0062, worksheet gap
|
|
5547
|
+
* G3). On the placement lane this is the winning placement entry's handle —
|
|
5548
|
+
* `PlacementOutput.rule_id`, already selected by the local resolver and
|
|
5549
|
+
* discarded at emit until now.
|
|
5550
|
+
*
|
|
5551
|
+
* Optional AND nullable, unlike the required-but-nullable siblings around
|
|
5552
|
+
* it. Every SDK already in the field emits these payloads without the key,
|
|
5553
|
+
* and `quarantineVerdict` (revturbine-web `src/lib/events/ingest-validation.ts`)
|
|
5554
|
+
* holds platform event names to this contract at the ingest boundary — a
|
|
5555
|
+
* required field would quarantine live telemetry from every deployed SDK.
|
|
5556
|
+
* So: absent = a pre-BL-0062 producer, `null` = no rule matched, string =
|
|
5557
|
+
* the winning rule.
|
|
5558
|
+
*
|
|
5559
|
+
* Why it rides the whole lifecycle base rather than `placement_resolved`
|
|
5560
|
+
* alone: attribution reads the exposure and outcome facts and stamps the
|
|
5561
|
+
* winning touch's keys onto the movement — "through it rule `usage_70pct`"
|
|
5562
|
+
* in the metric-primitives worked example (§3), anchored `touch_time` per
|
|
5563
|
+
* its R8. An exposure without the rule key cannot carry that.
|
|
5564
|
+
*/
|
|
5565
|
+
rule_handle: onstr()
|
|
5530
5566
|
};
|
|
5531
5567
|
var PlacementLifecyclePayload = z18.looseObject({ ...placementLifecycleBase });
|
|
5532
5568
|
var PlacementExposedPayload = z18.looseObject({
|
|
@@ -5551,7 +5587,29 @@ var PlacementInteractionPayload = z18.looseObject({
|
|
|
5551
5587
|
// explicit null when the caller supplied no timestamp (plan 228 TASK-4 —
|
|
5552
5588
|
// the typed emit surface surfaced the mismatch; the wire is the truth).
|
|
5553
5589
|
interaction_at: onstr(),
|
|
5554
|
-
remind_after_seconds: z18.number().optional().meta(Unrestricted15)
|
|
5590
|
+
remind_after_seconds: z18.number().optional().meta(Unrestricted15),
|
|
5591
|
+
/**
|
|
5592
|
+
* The `unique_handle` of the placement rule whose treatment the user acted
|
|
5593
|
+
* on — `PlacementOutput.rule_id`, the same value the lifecycle lane stamps
|
|
5594
|
+
* (BL-0182, follow-up to BL-0062 / #389).
|
|
5595
|
+
*
|
|
5596
|
+
* `placement_interaction` does not share `placementLifecycleBase` — its
|
|
5597
|
+
* placement keys are all optional, because a bare `trackInteraction` caller
|
|
5598
|
+
* may supply none of them — so #389's single edit to that base reached the
|
|
5599
|
+
* four lifecycle, five slot and three gate events and left the interaction
|
|
5600
|
+
* lane without a rule key. The effect was a funnel sliceable by rule at
|
|
5601
|
+
* exposure and at outcome but NOT at the click in between, which is the
|
|
5602
|
+
* step the CTR and click-through primitives are computed over.
|
|
5603
|
+
*
|
|
5604
|
+
* Same contract as #389 and for the same reason: optional AND nullable.
|
|
5605
|
+
* revturbine-web's `quarantineVerdict`
|
|
5606
|
+
* (`src/lib/events/ingest-validation.ts`) holds platform event names to
|
|
5607
|
+
* these contracts at the ingest boundary, so a required field would
|
|
5608
|
+
* quarantine live interaction telemetry from every SDK already deployed.
|
|
5609
|
+
* Absent = a pre-BL-0182 producer or an interaction with no decision in
|
|
5610
|
+
* scope, `null` = no rule matched, string = the winning rule.
|
|
5611
|
+
*/
|
|
5612
|
+
rule_handle: onstr()
|
|
5555
5613
|
});
|
|
5556
5614
|
var GateEvaluatedPayloadSchema = z18.looseObject({
|
|
5557
5615
|
entitlement_handle: str(),
|
|
@@ -5560,13 +5618,38 @@ var GateEvaluatedPayloadSchema = z18.looseObject({
|
|
|
5560
5618
|
reason: nstr(),
|
|
5561
5619
|
limit: nnum(),
|
|
5562
5620
|
used: nnum(),
|
|
5563
|
-
remaining: nnum()
|
|
5621
|
+
remaining: nnum(),
|
|
5622
|
+
/**
|
|
5623
|
+
* The `unique_handle` of the entitlement rule whose verdict this event
|
|
5624
|
+
* records; `null` when no rule matched (BL-0062, worksheet gap G3).
|
|
5625
|
+
*
|
|
5626
|
+
* This is the rule the §2.6.5 most-permissive selection picked — the one
|
|
5627
|
+
* whose `limit` / `enabled` / `allowance_value` produced the `outcome`
|
|
5628
|
+
* beside it. Without it the event says WHAT was decided and never WHICH
|
|
5629
|
+
* configured rule decided it, which is the rule slice, and the
|
|
5630
|
+
* `rule_handle → objective` hop the objective slice needs (BL-0065).
|
|
5631
|
+
*
|
|
5632
|
+
* Optional as well as nullable — see `placementLifecycleBase.rule_handle`
|
|
5633
|
+
* for why a required field would quarantine every deployed SDK's gate
|
|
5634
|
+
* telemetry at the ingest boundary.
|
|
5635
|
+
*/
|
|
5636
|
+
rule_handle: onstr()
|
|
5564
5637
|
}).meta(meta4("GateEvaluatedPayload"));
|
|
5565
5638
|
var GateAttemptedPayload = z18.looseObject({ entitlement_handle: str() });
|
|
5566
|
-
var GateAllowedPayload = z18.looseObject({
|
|
5639
|
+
var GateAllowedPayload = z18.looseObject({
|
|
5640
|
+
entitlement_handle: str(),
|
|
5641
|
+
/** The rule that granted. See `GateEvaluatedPayloadSchema.rule_handle`. */
|
|
5642
|
+
rule_handle: onstr()
|
|
5643
|
+
});
|
|
5567
5644
|
var GateDeniedPayload = z18.looseObject({
|
|
5568
5645
|
entitlement_handle: str(),
|
|
5569
|
-
reason: onstr()
|
|
5646
|
+
reason: onstr(),
|
|
5647
|
+
/**
|
|
5648
|
+
* The rule that denied — `null` on the two identity-shaped denials
|
|
5649
|
+
* (`no_plan_identity`, `no_matching_entitlement_rule`), where no rule
|
|
5650
|
+
* matched at all. See `GateEvaluatedPayloadSchema.rule_handle`.
|
|
5651
|
+
*/
|
|
5652
|
+
rule_handle: onstr()
|
|
5570
5653
|
});
|
|
5571
5654
|
var slotContextBase = {
|
|
5572
5655
|
surface_slot_id: nstr(),
|
|
@@ -5577,7 +5660,18 @@ var slotContextBase = {
|
|
|
5577
5660
|
category: nstr(),
|
|
5578
5661
|
decision_source: nstr(),
|
|
5579
5662
|
reason_codes: z18.array(z18.string()).meta(Unrestricted15),
|
|
5580
|
-
decision_id: nstr()
|
|
5663
|
+
decision_id: nstr(),
|
|
5664
|
+
/**
|
|
5665
|
+
* The placement rule that won this slot — `PlacementOutput.rule_id`, in
|
|
5666
|
+
* scope in `slotContext()` and dropped there until now (BL-0062, gap G3).
|
|
5667
|
+
*
|
|
5668
|
+
* Slot events are a verdict lane, not merely a render log: `slot_filled`
|
|
5669
|
+
* says a rule won, `slot_suppressed` / `slot_empty` say none did. Carrying
|
|
5670
|
+
* the handle is what lets "which rule keeps losing its slot" be asked at
|
|
5671
|
+
* all — `reason_codes` beside it names WHY, never WHOSE. `null` on the
|
|
5672
|
+
* terminal-empty events, absent on a pre-BL-0062 producer.
|
|
5673
|
+
*/
|
|
5674
|
+
rule_handle: onstr()
|
|
5581
5675
|
};
|
|
5582
5676
|
var SlotLifecyclePayload = z18.looseObject({ ...slotContextBase });
|
|
5583
5677
|
var SlotErrorPayload = z18.looseObject({
|
|
@@ -6473,6 +6567,30 @@ var TreatmentInteractionInputSchema = z24.object({
|
|
|
6473
6567
|
message_block_id: z24.string().min(1).optional().meta(Unrestricted20),
|
|
6474
6568
|
experiment_id: z24.string().min(1).optional().meta(Unrestricted20),
|
|
6475
6569
|
variant_key: z24.string().min(1).optional().meta(Unrestricted20),
|
|
6570
|
+
/**
|
|
6571
|
+
* The `unique_handle` of the rule whose treatment was presented (BL-0200).
|
|
6572
|
+
*
|
|
6573
|
+
* #391 put `rule_handle` on the clickstream `placement_interaction` EVENT
|
|
6574
|
+
* and deliberately kept it off this wire, so the BASE
|
|
6575
|
+
* `placement_exposure_attribution` row this contract writes had no rule and
|
|
6576
|
+
* `treatment-interaction-rows.ts` hard-coded `rule_handle: null`. Only an
|
|
6577
|
+
* exposure that an attributed conversion later enriched carried one, which
|
|
6578
|
+
* is why the attribution plane's rule cut could serve conversion-grain
|
|
6579
|
+
* metrics and had to refuse presentation-grain ones BY NAME: computed over
|
|
6580
|
+
* the attributed subset, `presented_accounts` would report the accounts
|
|
6581
|
+
* that converted and `conversion.rate` ~100%.
|
|
6582
|
+
*
|
|
6583
|
+
* Carrying it here makes the rule a property of the EXPOSURE, known at
|
|
6584
|
+
* presentation time by the only party that holds the resolved
|
|
6585
|
+
* `PlacementOutput`, so those cuts become honestly servable.
|
|
6586
|
+
*
|
|
6587
|
+
* Optional AND nullable, the same contract #389/#391 chose and for the same
|
|
6588
|
+
* reason: a required field would reject every already-deployed SDK's
|
|
6589
|
+
* interactions at the ingest boundary. Absent = a pre-BL-0200 producer or
|
|
6590
|
+
* an interaction with no decision in scope, `null` = no rule matched, a
|
|
6591
|
+
* string = the winning rule.
|
|
6592
|
+
*/
|
|
6593
|
+
rule_handle: z24.string().min(1).nullable().optional().meta(Unrestricted20),
|
|
6476
6594
|
/**
|
|
6477
6595
|
* Caller-declared test traffic (plan 164) — mirrors `TrackEvent.test` so
|
|
6478
6596
|
* the presentation/interaction feed (the dashboard denominators) carries
|
|
@@ -9925,6 +10043,14 @@ var EntitlementCheckResultSchema = z34.object({
|
|
|
9925
10043
|
used: z34.number().optional().meta(Unrestricted28),
|
|
9926
10044
|
/** `max(0, limit - used)`. */
|
|
9927
10045
|
remaining: z34.number().optional().meta(Unrestricted28),
|
|
10046
|
+
/**
|
|
10047
|
+
* The `unique_handle` of the entitlement rule this verdict came from —
|
|
10048
|
+
* the winner of the §2.6.5 most-permissive selection (BL-0062, gap G3).
|
|
10049
|
+
* Absent when no rule produced the verdict (unknown handle, no plan
|
|
10050
|
+
* identity, default policy). This is the value an SDK stamps as the
|
|
10051
|
+
* gate event's `rule_handle`, grounding the analytics rule slice.
|
|
10052
|
+
*/
|
|
10053
|
+
rule_handle: z34.string().optional().meta(Unrestricted28),
|
|
9928
10054
|
/** Upsell placement to render when entitlement is denied. */
|
|
9929
10055
|
placement: PlacementDecisionOutputSchema.optional().meta(Unrestricted28)
|
|
9930
10056
|
}).meta({ id: "EntitlementCheckResult", "x-revturbine-schema-persistence": Transient27, "x-revturbine-schema-exposure": External13 });
|
|
@@ -10799,11 +10925,72 @@ var CATALOG = {
|
|
|
10799
10925
|
// both means every user on every plan — legal and occasionally intended,
|
|
10800
10926
|
// but indistinguishable from an unfinished rule after authoring. Reverse
|
|
10801
10927
|
// trial rules can't hit this (premium_plan_id is required).
|
|
10928
|
+
// ── Dangling handle references (BL-0124) ─────────────────────────────────
|
|
10929
|
+
// Four references nothing checked: each fails silently at runtime (the
|
|
10930
|
+
// trigger never arms, the trial grants nothing, the rule targets nobody, the
|
|
10931
|
+
// chip lowers to a literal no user carries), so a Playbook with a typo
|
|
10932
|
+
// validated clean and shipped dead config. `error_launch` per spec §2 — "a
|
|
10933
|
+
// reference whose target is created in a later step" is legitimate mid-draft
|
|
10934
|
+
// and must never reach launch.
|
|
10935
|
+
"VAL-PLC-08": {
|
|
10936
|
+
id: "VAL-PLC-08",
|
|
10937
|
+
severity: "error_launch",
|
|
10938
|
+
message: "This placement triggers on an entitlement that does not exist.",
|
|
10939
|
+
specRef: "config-validation.md \xA75.2 (BL-0124)"
|
|
10940
|
+
},
|
|
10941
|
+
"VAL-PLC-09": {
|
|
10942
|
+
id: "VAL-PLC-09",
|
|
10943
|
+
severity: "error_launch",
|
|
10944
|
+
message: "This payload targets a segment chip that matches no segment or plan.",
|
|
10945
|
+
specRef: "config-validation.md \xA75.2 (BL-0124)"
|
|
10946
|
+
},
|
|
10947
|
+
"VAL-PLN-08": {
|
|
10948
|
+
id: "VAL-PLN-08",
|
|
10949
|
+
severity: "error_launch",
|
|
10950
|
+
message: "This entitlement rule targets a plan, add-on, or variation that does not exist.",
|
|
10951
|
+
specRef: "config-validation.md \xA75.3 (BL-0124)"
|
|
10952
|
+
},
|
|
10953
|
+
"VAL-TRL-05": {
|
|
10954
|
+
id: "VAL-TRL-05",
|
|
10955
|
+
severity: "error_launch",
|
|
10956
|
+
message: "This trial rule references a plan that does not exist.",
|
|
10957
|
+
specRef: "config-validation.md \xA75.5 (BL-0124)"
|
|
10958
|
+
},
|
|
10802
10959
|
"VAL-TRL-04": {
|
|
10803
10960
|
id: "VAL-TRL-04",
|
|
10804
10961
|
severity: "warning",
|
|
10805
10962
|
message: "Trial rule '{rule}' targets every user on every plan (no plan, no segment). Confirm the widest possible audience is intended.",
|
|
10806
10963
|
specRef: "config-validation.md \xA75.8 (promoted \u2014 plan 174 TASK-7)"
|
|
10964
|
+
},
|
|
10965
|
+
// entitlement rule → entitlement (BL-0164): same dangling-reference family as
|
|
10966
|
+
// the four above, just the one reference §5.9's original sweep missed.
|
|
10967
|
+
"VAL-ENT-01": {
|
|
10968
|
+
id: "VAL-ENT-01",
|
|
10969
|
+
severity: "error_launch",
|
|
10970
|
+
message: "This entitlement rule links an entitlement that does not exist.",
|
|
10971
|
+
specRef: "config-validation.md \xA75.3 (BL-0164)"
|
|
10972
|
+
},
|
|
10973
|
+
// BL-0168: VAL-PLC-07's `plansKnown` precondition (every plan row carries a
|
|
10974
|
+
// non-empty `unique_handle`) fails silently for a DB-shape plan row that
|
|
10975
|
+
// spells the identifier `handle` — the whole check goes dark for the
|
|
10976
|
+
// Playbook with nothing recording that it did not run. `warning`, never
|
|
10977
|
+
// blocking: it is a meta-finding about check coverage, not a config defect.
|
|
10978
|
+
"VAL-PLC-07-SKIPPED": {
|
|
10979
|
+
id: "VAL-PLC-07-SKIPPED",
|
|
10980
|
+
severity: "warning",
|
|
10981
|
+
message: "VAL-PLC-07 ('targets no configured plan') could not run: the plan catalog carries a row with no usable unique_handle.",
|
|
10982
|
+
specRef: "config-validation.md \xA75.2 (BL-0168)"
|
|
10983
|
+
},
|
|
10984
|
+
// BL-0168: the payload→placement parent lookup (`placementPayloads`) matches
|
|
10985
|
+
// a standalone `placement_payloads` row on `row.id === payload.placement_id`
|
|
10986
|
+
// only. A miss leaves `category` undefined, `categoryBucket` falls through to
|
|
10987
|
+
// 99, and VAL-PLC-06's Access-Gate/Fixed cap check silently never runs for
|
|
10988
|
+
// that payload. `warning`, never blocking, same rationale as above.
|
|
10989
|
+
"VAL-PLC-06-SKIPPED": {
|
|
10990
|
+
id: "VAL-PLC-06-SKIPPED",
|
|
10991
|
+
severity: "warning",
|
|
10992
|
+
message: "VAL-PLC-06 (ignored cap/cooldown settings) could not run: this payload's placement_id matches no configured placement.",
|
|
10993
|
+
specRef: "config-validation.md \xA75.2 (BL-0168)"
|
|
10807
10994
|
}
|
|
10808
10995
|
};
|
|
10809
10996
|
function getCatalogEntry(id) {
|
|
@@ -10832,28 +11019,285 @@ function runSemanticRules(graph, opts = {}) {
|
|
|
10832
11019
|
...checkPlacementAuthoring(graph),
|
|
10833
11020
|
...checkTrialRuleWidestAudience(graph),
|
|
10834
11021
|
...checkSegmentExperimentRefs(graph),
|
|
10835
|
-
...checkExperimentMetricRefs(graph, opts.knownMetricIds)
|
|
11022
|
+
...checkExperimentMetricRefs(graph, opts.knownMetricIds),
|
|
11023
|
+
...checkDanglingReferences(graph)
|
|
11024
|
+
];
|
|
11025
|
+
}
|
|
11026
|
+
function referenceNamespace(graph, table2, handleField) {
|
|
11027
|
+
const rows = graph[table2];
|
|
11028
|
+
if (!rows) return void 0;
|
|
11029
|
+
const known = /* @__PURE__ */ new Set();
|
|
11030
|
+
for (const row of rows) {
|
|
11031
|
+
const handle = row[handleField];
|
|
11032
|
+
if (typeof handle !== "string" || !handle) return void 0;
|
|
11033
|
+
known.add(handle);
|
|
11034
|
+
}
|
|
11035
|
+
return known;
|
|
11036
|
+
}
|
|
11037
|
+
function knownHandlesHint(known, label) {
|
|
11038
|
+
if (known.size === 0) return "";
|
|
11039
|
+
const sorted = [...known].sort();
|
|
11040
|
+
const shown = sorted.slice(0, 8);
|
|
11041
|
+
const rest = sorted.length > shown.length ? `, \u2026 (${sorted.length} total)` : "";
|
|
11042
|
+
return ` Declared ${label}: ${shown.map((h) => `'${h}'`).join(", ")}${rest}.`;
|
|
11043
|
+
}
|
|
11044
|
+
function firstStringField(row, fields) {
|
|
11045
|
+
for (const field of fields) {
|
|
11046
|
+
const value = row[field];
|
|
11047
|
+
if (typeof value === "string" && value.length > 0) return { field, value };
|
|
11048
|
+
}
|
|
11049
|
+
return void 0;
|
|
11050
|
+
}
|
|
11051
|
+
function checkDanglingReferences(graph) {
|
|
11052
|
+
return [
|
|
11053
|
+
...checkTriggerEntitlementRefs(graph),
|
|
11054
|
+
...checkTrialPlanRefs(graph),
|
|
11055
|
+
...checkEntitlementRuleTargetRefs(graph),
|
|
11056
|
+
...checkEntitlementRuleEntitlementRefs(graph),
|
|
11057
|
+
...checkPayloadSegmentChipRefs(graph)
|
|
10836
11058
|
];
|
|
10837
11059
|
}
|
|
11060
|
+
function checkTriggerEntitlementRefs(graph) {
|
|
11061
|
+
const known = referenceNamespace(graph, "entitlements", "unique_handle");
|
|
11062
|
+
if (!known) return [];
|
|
11063
|
+
const findings = [];
|
|
11064
|
+
for (const [index, placement] of (graph.placements ?? []).entries()) {
|
|
11065
|
+
if (!isRecord2(placement.trigger)) continue;
|
|
11066
|
+
const ref = firstStringField(placement.trigger, ["entitlement_handle", "entitlement_id"]);
|
|
11067
|
+
if (!ref || known.has(ref.value)) continue;
|
|
11068
|
+
const id = String(placement.handle ?? placement.id ?? "");
|
|
11069
|
+
findings.push(
|
|
11070
|
+
finding(
|
|
11071
|
+
"VAL-PLC-08",
|
|
11072
|
+
{
|
|
11073
|
+
object_type: "placement",
|
|
11074
|
+
object_id: id,
|
|
11075
|
+
field: `trigger.${ref.field}`,
|
|
11076
|
+
path: ["placements", index, "trigger", ref.field],
|
|
11077
|
+
studio: "placements"
|
|
11078
|
+
},
|
|
11079
|
+
{
|
|
11080
|
+
message: `Placement '${id}' triggers on entitlement '${ref.value}', which no entitlement declares.`,
|
|
11081
|
+
detail: "A trigger's entitlement reference must be an entitlement's unique_handle, not its config id. A reference that matches nothing never arms the trigger, so the placement looks configured and can never show." + knownHandlesHint(known, "entitlement handles")
|
|
11082
|
+
}
|
|
11083
|
+
)
|
|
11084
|
+
);
|
|
11085
|
+
}
|
|
11086
|
+
return findings;
|
|
11087
|
+
}
|
|
11088
|
+
var TRIAL_PLAN_FIELDS = ["plan_id", "convert_to_plan_id", "fallback_plan_id", "premium_plan_id"];
|
|
11089
|
+
function checkTrialPlanRefs(graph) {
|
|
11090
|
+
const known = referenceNamespace(graph, "plans", "unique_handle");
|
|
11091
|
+
if (!known) return [];
|
|
11092
|
+
const findings = [];
|
|
11093
|
+
for (const table2 of ["free_trial_rules", "reverse_trial_rules"]) {
|
|
11094
|
+
const objectType = table2 === "free_trial_rules" ? "free_trial_rule" : "reverse_trial_rule";
|
|
11095
|
+
for (const [index, rule] of (graph[table2] ?? []).entries()) {
|
|
11096
|
+
const id = String(rule.handle ?? rule.id ?? "");
|
|
11097
|
+
for (const field of TRIAL_PLAN_FIELDS) {
|
|
11098
|
+
const value = rule[field];
|
|
11099
|
+
if (typeof value !== "string" || value.length === 0 || known.has(value)) continue;
|
|
11100
|
+
findings.push(
|
|
11101
|
+
finding(
|
|
11102
|
+
"VAL-TRL-05",
|
|
11103
|
+
{
|
|
11104
|
+
object_type: objectType,
|
|
11105
|
+
object_id: id,
|
|
11106
|
+
field,
|
|
11107
|
+
path: [table2, index, field],
|
|
11108
|
+
studio: "trials"
|
|
11109
|
+
},
|
|
11110
|
+
{
|
|
11111
|
+
message: `Trial rule '${id || "unnamed"}' references plan '${value}' in ${field}, which no plan declares.`,
|
|
11112
|
+
detail: "Trial plan references must be a plan's unique_handle, not its config id. A reference that matches nothing makes the rule grant or convert nothing, and the trial silently never runs." + knownHandlesHint(known, "plan handles")
|
|
11113
|
+
}
|
|
11114
|
+
)
|
|
11115
|
+
);
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
return findings;
|
|
11120
|
+
}
|
|
11121
|
+
var TARGET_KIND_TABLES = {
|
|
11122
|
+
plan: { table: "plans", handleField: "unique_handle", label: "plan" },
|
|
11123
|
+
addon: { table: "addons", handleField: "unique_handle", label: "add-on" },
|
|
11124
|
+
plan_variation: { table: "plan_variations", handleField: "handle", label: "plan variation" },
|
|
11125
|
+
addon_variation: { table: "addon_variations", handleField: "handle", label: "add-on variation" }
|
|
11126
|
+
};
|
|
11127
|
+
function checkEntitlementRuleTargetRefs(graph) {
|
|
11128
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
11129
|
+
const namespaceFor = (kind) => {
|
|
11130
|
+
const spec = TARGET_KIND_TABLES[kind];
|
|
11131
|
+
if (!spec) return void 0;
|
|
11132
|
+
if (!namespaces.has(kind)) namespaces.set(kind, referenceNamespace(graph, spec.table, spec.handleField));
|
|
11133
|
+
return namespaces.get(kind);
|
|
11134
|
+
};
|
|
11135
|
+
const findings = [];
|
|
11136
|
+
for (const [index, rule] of (graph.entitlement_rules ?? []).entries()) {
|
|
11137
|
+
const id = String(rule.handle ?? rule.id ?? "");
|
|
11138
|
+
const refs = [];
|
|
11139
|
+
if (Array.isArray(rule.targets)) {
|
|
11140
|
+
for (const [targetIndex, target] of rule.targets.entries()) {
|
|
11141
|
+
if (!isRecord2(target) || typeof target.id !== "string" || target.id.length === 0) continue;
|
|
11142
|
+
const kind = typeof target.kind === "string" ? target.kind : "plan";
|
|
11143
|
+
refs.push({
|
|
11144
|
+
kind,
|
|
11145
|
+
value: target.id,
|
|
11146
|
+
field: `targets.${targetIndex}.id`,
|
|
11147
|
+
path: ["entitlement_rules", index, "targets", targetIndex, "id"]
|
|
11148
|
+
});
|
|
11149
|
+
}
|
|
11150
|
+
} else if (Array.isArray(rule.plan_ids)) {
|
|
11151
|
+
for (const [planIndex, planRef] of rule.plan_ids.entries()) {
|
|
11152
|
+
if (typeof planRef !== "string" || planRef.length === 0) continue;
|
|
11153
|
+
refs.push({
|
|
11154
|
+
kind: "plan",
|
|
11155
|
+
value: planRef,
|
|
11156
|
+
field: `plan_ids.${planIndex}`,
|
|
11157
|
+
path: ["entitlement_rules", index, "plan_ids", planIndex]
|
|
11158
|
+
});
|
|
11159
|
+
}
|
|
11160
|
+
} else if (typeof rule.plan_id === "string" && rule.plan_id.length > 0) {
|
|
11161
|
+
refs.push({
|
|
11162
|
+
kind: "plan",
|
|
11163
|
+
value: rule.plan_id,
|
|
11164
|
+
field: "plan_id",
|
|
11165
|
+
path: ["entitlement_rules", index, "plan_id"]
|
|
11166
|
+
});
|
|
11167
|
+
}
|
|
11168
|
+
for (const ref of refs) {
|
|
11169
|
+
const known = namespaceFor(ref.kind);
|
|
11170
|
+
if (!known || known.has(ref.value)) continue;
|
|
11171
|
+
const label = TARGET_KIND_TABLES[ref.kind]?.label ?? ref.kind;
|
|
11172
|
+
findings.push(
|
|
11173
|
+
finding(
|
|
11174
|
+
"VAL-PLN-08",
|
|
11175
|
+
{
|
|
11176
|
+
object_type: "entitlement_rule",
|
|
11177
|
+
object_id: id,
|
|
11178
|
+
field: ref.field,
|
|
11179
|
+
path: ref.path,
|
|
11180
|
+
studio: "plans-entitlements"
|
|
11181
|
+
},
|
|
11182
|
+
{
|
|
11183
|
+
message: `Entitlement rule '${id || "unnamed"}' targets ${label} '${ref.value}', which no ${label} declares.`,
|
|
11184
|
+
detail: "Rule targets must be the target's unique_handle, not its config id. A target that matches nothing makes the rule apply to no one, so the entitlement it grants is silently absent for every user." + knownHandlesHint(known, `${label} handles`)
|
|
11185
|
+
}
|
|
11186
|
+
)
|
|
11187
|
+
);
|
|
11188
|
+
}
|
|
11189
|
+
}
|
|
11190
|
+
return findings;
|
|
11191
|
+
}
|
|
11192
|
+
function checkEntitlementRuleEntitlementRefs(graph) {
|
|
11193
|
+
const known = referenceNamespace(graph, "entitlements", "unique_handle");
|
|
11194
|
+
if (!known) return [];
|
|
11195
|
+
const findings = [];
|
|
11196
|
+
for (const [index, rule] of (graph.entitlement_rules ?? []).entries()) {
|
|
11197
|
+
const value = rule.entitlement_id;
|
|
11198
|
+
if (typeof value !== "string" || value.length === 0 || known.has(value)) continue;
|
|
11199
|
+
const id = String(rule.handle ?? rule.id ?? "");
|
|
11200
|
+
findings.push(
|
|
11201
|
+
finding(
|
|
11202
|
+
"VAL-ENT-01",
|
|
11203
|
+
{
|
|
11204
|
+
object_type: "entitlement_rule",
|
|
11205
|
+
object_id: id,
|
|
11206
|
+
field: "entitlement_id",
|
|
11207
|
+
path: ["entitlement_rules", index, "entitlement_id"],
|
|
11208
|
+
studio: "plans-entitlements"
|
|
11209
|
+
},
|
|
11210
|
+
{
|
|
11211
|
+
message: `Entitlement rule '${id || "unnamed"}' links entitlement '${value}', which no entitlement declares.`,
|
|
11212
|
+
detail: "A rule's entitlement reference must be an entitlement's unique_handle, not its config id. A reference that matches nothing means the entitlement it grants or limits is silently absent for every user." + knownHandlesHint(known, "entitlement handles")
|
|
11213
|
+
}
|
|
11214
|
+
)
|
|
11215
|
+
);
|
|
11216
|
+
}
|
|
11217
|
+
return findings;
|
|
11218
|
+
}
|
|
11219
|
+
function payloadSegmentChips(payload, path3) {
|
|
11220
|
+
const target = isRecord2(payload.target) ? payload.target : void 0;
|
|
11221
|
+
if (target && Array.isArray(target.segment_chips)) {
|
|
11222
|
+
return { fields: ["target", "segment_chips"], path: [...path3, "target", "segment_chips"], chips: target.segment_chips };
|
|
11223
|
+
}
|
|
11224
|
+
if (Array.isArray(payload.target_segment_chips)) {
|
|
11225
|
+
return { fields: ["target_segment_chips"], path: [...path3, "target_segment_chips"], chips: payload.target_segment_chips };
|
|
11226
|
+
}
|
|
11227
|
+
return void 0;
|
|
11228
|
+
}
|
|
11229
|
+
function checkPayloadSegmentChipRefs(graph) {
|
|
11230
|
+
const segments = referenceNamespace(graph, "segments", "handle");
|
|
11231
|
+
const plans = referenceNamespace(graph, "plans", "unique_handle");
|
|
11232
|
+
if (!segments || !plans) return [];
|
|
11233
|
+
const findings = [];
|
|
11234
|
+
for (const { payload, path: path3 } of placementPayloads(graph)) {
|
|
11235
|
+
const chips = payloadSegmentChips(payload, path3);
|
|
11236
|
+
if (!chips) continue;
|
|
11237
|
+
const id = String(payload.payload_id ?? payload.id ?? "");
|
|
11238
|
+
for (const [chipIndex, raw] of chips.chips.entries()) {
|
|
11239
|
+
if (typeof raw !== "string") continue;
|
|
11240
|
+
const value = raw.trim();
|
|
11241
|
+
if (!value || segments.has(value) || plans.has(value)) continue;
|
|
11242
|
+
findings.push(
|
|
11243
|
+
finding(
|
|
11244
|
+
"VAL-PLC-09",
|
|
11245
|
+
{
|
|
11246
|
+
object_type: "placement_payload",
|
|
11247
|
+
object_id: id,
|
|
11248
|
+
field: `${chips.fields.join(".")}.${chipIndex}`,
|
|
11249
|
+
path: [...chips.path, chipIndex],
|
|
11250
|
+
studio: "placements"
|
|
11251
|
+
},
|
|
11252
|
+
{
|
|
11253
|
+
message: `Payload '${id}' targets segment chip '${value}', which matches no segment or plan.`,
|
|
11254
|
+
detail: "A chip resolves against segment handles first, then plan handles \u2014 never a config id or a display name. An unresolved chip compiles to a literal no user carries, so the payload can never be shown." + knownHandlesHint(segments, "segment handles")
|
|
11255
|
+
}
|
|
11256
|
+
)
|
|
11257
|
+
);
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
return findings;
|
|
11261
|
+
}
|
|
10838
11262
|
function* placementPayloads(graph) {
|
|
10839
11263
|
const placements = graph.placements ?? [];
|
|
10840
11264
|
for (const [index, placement] of placements.entries()) {
|
|
10841
11265
|
if (!Array.isArray(placement.payloads)) continue;
|
|
10842
11266
|
for (const [payloadIndex, payload] of placement.payloads.entries()) {
|
|
10843
|
-
if (isRecord2(payload)) yield { payload, category: placement.category, path: ["placements", index, "payloads", payloadIndex] };
|
|
11267
|
+
if (isRecord2(payload)) yield { payload, category: placement.category, path: ["placements", index, "payloads", payloadIndex], parentFound: true };
|
|
10844
11268
|
}
|
|
10845
11269
|
}
|
|
10846
11270
|
for (const [index, payload] of (graph.placement_payloads ?? []).entries()) {
|
|
10847
11271
|
const placement = placements.find((row) => typeof row.id === "string" && row.id === payload.placement_id);
|
|
10848
|
-
yield { payload, category: placement?.category, path: ["placement_payloads", index] };
|
|
11272
|
+
yield { payload, category: placement?.category, path: ["placement_payloads", index], parentFound: placement !== void 0 };
|
|
10849
11273
|
}
|
|
10850
11274
|
}
|
|
10851
11275
|
function checkPlacementAuthoring(graph) {
|
|
10852
11276
|
const findings = [];
|
|
10853
|
-
const
|
|
10854
|
-
const
|
|
10855
|
-
|
|
11277
|
+
const plans = graph.plans;
|
|
11278
|
+
const plansKnown = Array.isArray(plans) && plans.every((plan) => typeof plan.unique_handle === "string" && plan.unique_handle.length > 0);
|
|
11279
|
+
const planHandles = new Set((plans ?? []).map((plan) => plan.unique_handle));
|
|
11280
|
+
if (Array.isArray(plans) && plans.length > 0 && !plansKnown) {
|
|
11281
|
+
findings.push(finding("VAL-PLC-07-SKIPPED", {
|
|
11282
|
+
object_type: "plan",
|
|
11283
|
+
studio: "placements"
|
|
11284
|
+
}, {
|
|
11285
|
+
detail: "Every plan row needs a non-empty unique_handle for VAL-PLC-07 to run against this Playbook. A DB-shape row that spells the identifier `handle` does not satisfy it."
|
|
11286
|
+
}));
|
|
11287
|
+
}
|
|
11288
|
+
for (const { payload, category, path: path3, parentFound } of placementPayloads(graph)) {
|
|
10856
11289
|
const id = String(payload.payload_id ?? payload.id ?? "");
|
|
11290
|
+
if (!parentFound) {
|
|
11291
|
+
findings.push(finding("VAL-PLC-06-SKIPPED", {
|
|
11292
|
+
object_type: "placement_payload",
|
|
11293
|
+
object_id: id,
|
|
11294
|
+
field: "placement_id",
|
|
11295
|
+
studio: "placements"
|
|
11296
|
+
}, {
|
|
11297
|
+
message: `Payload '${id}' names a placement_id that matches no configured placement, so VAL-PLC-06's cap/cooldown check could not run for it.`,
|
|
11298
|
+
detail: "Fix placement_id (or add the placement it should reference) to restore the cap check for Access Gate and Fixed placements."
|
|
11299
|
+
}));
|
|
11300
|
+
}
|
|
10857
11301
|
const bucket = typeof category === "string" ? categoryBucket(category) : 99;
|
|
10858
11302
|
if (bucket === 0 || bucket === 1) {
|
|
10859
11303
|
const caps = isRecord2(payload.caps) ? payload.caps : {};
|
|
@@ -11506,6 +11950,7 @@ var CtaActionTypeSchema2 = z39.enum([
|
|
|
11506
11950
|
"snooze",
|
|
11507
11951
|
"custom"
|
|
11508
11952
|
]).meta({ id: "CtaActionType", "x-revturbine-schema-persistence": Transient30, "x-revturbine-schema-exposure": External14 });
|
|
11953
|
+
var ObjectiveField2 = HandleField2.optional();
|
|
11509
11954
|
var IdentityKind2 = {
|
|
11510
11955
|
/** Author-given, human-meaningful handle (plans, entitlements, segments, …). */
|
|
11511
11956
|
Named: "named",
|
|
@@ -11711,6 +12156,12 @@ var EntitlementRuleSchema2 = IdField2.merge(TimestampFields2).merge(TenantIdFiel
|
|
|
11711
12156
|
// "match all users" (replaces the legacy 'all' sentinel).
|
|
11712
12157
|
segment_ids: z62.array(z62.string()).default([]).meta(Unrestricted210),
|
|
11713
12158
|
visibility: RuleVisibilitySchema2.default("public").meta(Unrestricted210),
|
|
12159
|
+
// Business objective this rule monetizes for — the analytics `objective`
|
|
12160
|
+
// slice, which resolves `rule_handle → objective` through a configuration
|
|
12161
|
+
// snapshot (BL-0065 / worksheet G4). Config-only, and deliberately NOT part
|
|
12162
|
+
// of the minted identity below: an objective relabel is a behaviour-only
|
|
12163
|
+
// edit that coalesces onto the same rule, not a new rule scope.
|
|
12164
|
+
objective: ObjectiveField2.meta(Unrestricted210),
|
|
11714
12165
|
// Usage-Limit "measured over" window, rule-level (plan #55). Rate Limit
|
|
11715
12166
|
// keeps its entitlement-level `period_scope`; this is the per-rule one.
|
|
11716
12167
|
period_scope: UsagePeriodScopeSchema2.optional().meta(Unrestricted210),
|
|
@@ -13934,7 +14385,7 @@ function repairDeprecatedFields(config, schema = PlaybookObjectSchema2) {
|
|
|
13934
14385
|
}
|
|
13935
14386
|
|
|
13936
14387
|
// src/schema/version.ts
|
|
13937
|
-
var SCHEMA_VERSION = "0.1.
|
|
14388
|
+
var SCHEMA_VERSION = "0.1.343";
|
|
13938
14389
|
|
|
13939
14390
|
// src/lib/credentials.ts
|
|
13940
14391
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
@@ -14283,7 +14734,13 @@ function hasBlockingFindings(findings, force = false) {
|
|
|
14283
14734
|
}
|
|
14284
14735
|
function formatFindings(findings) {
|
|
14285
14736
|
if (findings.length === 0) return "No validation findings.";
|
|
14286
|
-
return findings.map((f) =>
|
|
14737
|
+
return findings.map((f) => {
|
|
14738
|
+
const blocking = isBlockingFinding(f);
|
|
14739
|
+
const head = `${blocking ? "\u2717" : "\u2022"} [${f.severity}] ${f.code}: ${f.message}`;
|
|
14740
|
+
const detail = f.detail?.trim();
|
|
14741
|
+
return blocking && detail ? `${head}
|
|
14742
|
+
${detail}` : head;
|
|
14743
|
+
}).join("\n");
|
|
14287
14744
|
}
|
|
14288
14745
|
async function fetchValidation(baseUrl, playbookVersionId, headers, fetchImpl = fetch) {
|
|
14289
14746
|
const res = await fetchImpl(`${baseUrl}/api/config/validate`, {
|
|
@@ -14390,28 +14847,181 @@ function formatAuthentication(hasToken, tokenValid) {
|
|
|
14390
14847
|
return "authentication: verification unavailable \u2014 credentials present; retry when the instance is reachable";
|
|
14391
14848
|
}
|
|
14392
14849
|
|
|
14850
|
+
// src/lib/evaluate-context.ts
|
|
14851
|
+
function isRecord4(value) {
|
|
14852
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14853
|
+
}
|
|
14854
|
+
var DEFAULT_USAGE_UNIT = "unit";
|
|
14855
|
+
function normalizeUsageEntry(handle, value, errors) {
|
|
14856
|
+
if (typeof value === "number") {
|
|
14857
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
14858
|
+
errors.push(`usage.${handle} must be a finite number \u2265 0`);
|
|
14859
|
+
return void 0;
|
|
14860
|
+
}
|
|
14861
|
+
return { entitlement_handle: handle, unit: DEFAULT_USAGE_UNIT, amount: value };
|
|
14862
|
+
}
|
|
14863
|
+
if (!isRecord4(value)) {
|
|
14864
|
+
errors.push(`usage.${handle} must be a number or an object with "amount"`);
|
|
14865
|
+
return void 0;
|
|
14866
|
+
}
|
|
14867
|
+
const amount = value.amount;
|
|
14868
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
14869
|
+
errors.push(`usage.${handle}.amount must be a finite number \u2265 0`);
|
|
14870
|
+
return void 0;
|
|
14871
|
+
}
|
|
14872
|
+
const entry = {
|
|
14873
|
+
entitlement_handle: typeof value.entitlement_handle === "string" ? value.entitlement_handle : handle,
|
|
14874
|
+
unit: typeof value.unit === "string" && value.unit.length > 0 ? value.unit : DEFAULT_USAGE_UNIT,
|
|
14875
|
+
amount
|
|
14876
|
+
};
|
|
14877
|
+
if (value.limit !== void 0) {
|
|
14878
|
+
if (typeof value.limit !== "number" || !Number.isFinite(value.limit) || value.limit < 0) {
|
|
14879
|
+
errors.push(`usage.${handle}.limit must be a finite number \u2265 0`);
|
|
14880
|
+
} else {
|
|
14881
|
+
entry.limit = value.limit;
|
|
14882
|
+
}
|
|
14883
|
+
}
|
|
14884
|
+
if (value.reset_date !== void 0) {
|
|
14885
|
+
if (typeof value.reset_date !== "string") {
|
|
14886
|
+
errors.push(`usage.${handle}.reset_date must be a string`);
|
|
14887
|
+
} else {
|
|
14888
|
+
entry.reset_date = value.reset_date;
|
|
14889
|
+
}
|
|
14890
|
+
}
|
|
14891
|
+
return entry;
|
|
14892
|
+
}
|
|
14893
|
+
function normalizeUsage(value, errors) {
|
|
14894
|
+
if (value === void 0 || value === null) return void 0;
|
|
14895
|
+
if (!isRecord4(value)) {
|
|
14896
|
+
errors.push("usage must be an object keyed by entitlement handle");
|
|
14897
|
+
return void 0;
|
|
14898
|
+
}
|
|
14899
|
+
const out = {};
|
|
14900
|
+
for (const [handle, raw] of Object.entries(value)) {
|
|
14901
|
+
const entry = normalizeUsageEntry(handle, raw, errors);
|
|
14902
|
+
if (entry) out[handle] = entry;
|
|
14903
|
+
}
|
|
14904
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
14905
|
+
}
|
|
14906
|
+
function normalizeTrialStatus(value, errors) {
|
|
14907
|
+
if (value === void 0 || value === null) return void 0;
|
|
14908
|
+
if (!isRecord4(value)) {
|
|
14909
|
+
errors.push("trial status must be an object (the SDK's UserTrialStatus)");
|
|
14910
|
+
return void 0;
|
|
14911
|
+
}
|
|
14912
|
+
if (typeof value.in_trial !== "boolean") {
|
|
14913
|
+
errors.push('trial status must carry a boolean "in_trial"');
|
|
14914
|
+
return void 0;
|
|
14915
|
+
}
|
|
14916
|
+
return value;
|
|
14917
|
+
}
|
|
14918
|
+
function normalizeTrialInstances(value, errors) {
|
|
14919
|
+
if (value === void 0 || value === null) return void 0;
|
|
14920
|
+
if (!Array.isArray(value) || value.some((item) => !isRecord4(item))) {
|
|
14921
|
+
errors.push("trial_instances must be an array of TrialInstance objects");
|
|
14922
|
+
return void 0;
|
|
14923
|
+
}
|
|
14924
|
+
return value.length > 0 ? value : void 0;
|
|
14925
|
+
}
|
|
14926
|
+
function normalizeNowIso(value, source, errors) {
|
|
14927
|
+
if (value === void 0 || value === null) return void 0;
|
|
14928
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
14929
|
+
errors.push(`${source} must be an ISO-8601 timestamp (e.g. 2026-09-21T00:00:00Z)`);
|
|
14930
|
+
return void 0;
|
|
14931
|
+
}
|
|
14932
|
+
return new Date(value).toISOString();
|
|
14933
|
+
}
|
|
14934
|
+
function parseJsonFlag(raw, flag, errors) {
|
|
14935
|
+
try {
|
|
14936
|
+
return JSON.parse(raw);
|
|
14937
|
+
} catch (err) {
|
|
14938
|
+
errors.push(`${flag} must be valid JSON: ${err.message}`);
|
|
14939
|
+
return void 0;
|
|
14940
|
+
}
|
|
14941
|
+
}
|
|
14942
|
+
function parseDecisionContext(input) {
|
|
14943
|
+
const errors = [];
|
|
14944
|
+
const usageSource = input.usage !== void 0 ? parseJsonFlag(input.usage, "--usage", errors) : input.ctx.usage;
|
|
14945
|
+
const usage = normalizeUsage(usageSource, errors);
|
|
14946
|
+
const trialSource = input.trialStatus !== void 0 ? parseJsonFlag(input.trialStatus, "--trial-status", errors) : input.ctx.trial;
|
|
14947
|
+
const trialStatus = normalizeTrialStatus(trialSource, errors);
|
|
14948
|
+
const trialInstances = normalizeTrialInstances(input.ctx.trial_instances, errors);
|
|
14949
|
+
const nowIso = input.now !== void 0 ? normalizeNowIso(input.now, "--now", errors) : normalizeNowIso(input.ctx.now_iso, "now_iso", errors);
|
|
14950
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
14951
|
+
return {
|
|
14952
|
+
ok: true,
|
|
14953
|
+
context: {
|
|
14954
|
+
...usage ? { usage } : {},
|
|
14955
|
+
...trialStatus ? { trialStatus } : {},
|
|
14956
|
+
...trialInstances ? { trialInstances } : {},
|
|
14957
|
+
...nowIso ? { nowIso } : {}
|
|
14958
|
+
}
|
|
14959
|
+
};
|
|
14960
|
+
}
|
|
14961
|
+
|
|
14393
14962
|
// src/lib/evaluate-local.ts
|
|
14394
14963
|
import { initRevTurbine } from "@revturbine/sdk/headless";
|
|
14395
14964
|
function resolvePlacementComponentType(input) {
|
|
14396
14965
|
return input.componentType ?? input.surfaceType;
|
|
14397
14966
|
}
|
|
14967
|
+
function payloadIdOf(decision) {
|
|
14968
|
+
if (typeof decision !== "object" || decision === null) return null;
|
|
14969
|
+
const output = decision.output;
|
|
14970
|
+
if (typeof output !== "object" || output === null) return null;
|
|
14971
|
+
const id = output.output_id;
|
|
14972
|
+
return typeof id === "string" ? id : null;
|
|
14973
|
+
}
|
|
14974
|
+
async function completeUsageLimits(session, usage) {
|
|
14975
|
+
let completed = false;
|
|
14976
|
+
const filled = {};
|
|
14977
|
+
for (const [handle, entry] of Object.entries(usage)) {
|
|
14978
|
+
if (entry.limit !== void 0) {
|
|
14979
|
+
filled[handle] = entry;
|
|
14980
|
+
continue;
|
|
14981
|
+
}
|
|
14982
|
+
const result = await session.checkEntitlement(entry.entitlement_handle);
|
|
14983
|
+
if (typeof result?.limit === "number" && Number.isFinite(result.limit)) {
|
|
14984
|
+
filled[handle] = { ...entry, limit: result.limit };
|
|
14985
|
+
completed = true;
|
|
14986
|
+
} else {
|
|
14987
|
+
filled[handle] = entry;
|
|
14988
|
+
}
|
|
14989
|
+
}
|
|
14990
|
+
if (completed) session.setUserContext({ usage: filled });
|
|
14991
|
+
}
|
|
14398
14992
|
async function evaluateLocal(config, input) {
|
|
14399
14993
|
const user = { id: input.userId };
|
|
14400
14994
|
if (input.planHandle) user.plan_handle = input.planHandle;
|
|
14401
14995
|
if (input.traits && Object.keys(input.traits).length > 0) user.custom = input.traits;
|
|
14996
|
+
if (input.usage) user.usage = input.usage;
|
|
14402
14997
|
const session = await initRevTurbine({
|
|
14403
14998
|
user,
|
|
14404
|
-
localRuntime: {
|
|
14999
|
+
localRuntime: {
|
|
15000
|
+
playbook: config,
|
|
15001
|
+
...input.trialStatus ? { initialData: { trialStatus: input.trialStatus } } : {}
|
|
15002
|
+
}
|
|
14405
15003
|
});
|
|
15004
|
+
if (input.trialInstances) {
|
|
15005
|
+
await session.sdk.setTrialInstances(
|
|
15006
|
+
input.trialInstances,
|
|
15007
|
+
input.nowIso ? { nowIso: input.nowIso } : void 0
|
|
15008
|
+
);
|
|
15009
|
+
}
|
|
15010
|
+
const trialAsked = Boolean(input.trialInstances || input.trialStatus);
|
|
15011
|
+
const trialStatus = trialAsked ? await session.getTrialStatus() : void 0;
|
|
15012
|
+
if (input.usage) await completeUsageLimits(session, input.usage);
|
|
14406
15013
|
const entitlements = {};
|
|
14407
15014
|
for (const handle of input.entitlementHandles ?? []) {
|
|
14408
15015
|
entitlements[handle] = await session.checkEntitlement(handle);
|
|
14409
15016
|
}
|
|
14410
15017
|
const decisions = [];
|
|
15018
|
+
const payloadIds = {};
|
|
14411
15019
|
for (const placementId of input.placementIds ?? []) {
|
|
14412
15020
|
const controller = session.placement({ placement: { name: placementId } });
|
|
14413
15021
|
await controller.load();
|
|
14414
|
-
|
|
15022
|
+
const decision = controller.state.decision;
|
|
15023
|
+
decisions.push(decision);
|
|
15024
|
+
payloadIds[placementId] = payloadIdOf(decision);
|
|
14415
15025
|
}
|
|
14416
15026
|
const componentType = input.slot ? resolvePlacementComponentType(input.slot) : void 0;
|
|
14417
15027
|
const placement = input.slot && (input.slot.slotId || componentType) ? await session.getPlacement({
|
|
@@ -14419,7 +15029,14 @@ async function evaluateLocal(config, input) {
|
|
|
14419
15029
|
...componentType ? { componentType } : {},
|
|
14420
15030
|
...input.planHandle ? { planHandle: input.planHandle } : {}
|
|
14421
15031
|
}) : null;
|
|
14422
|
-
return {
|
|
15032
|
+
return {
|
|
15033
|
+
decisions,
|
|
15034
|
+
payloadIds,
|
|
15035
|
+
entitlements,
|
|
15036
|
+
placement,
|
|
15037
|
+
personalizationTokens: session.sdk.getPersonalizationTokens(),
|
|
15038
|
+
...trialStatus !== void 0 ? { trialStatus } : {}
|
|
15039
|
+
};
|
|
14423
15040
|
}
|
|
14424
15041
|
|
|
14425
15042
|
// src/lib/ingest-keys.ts
|
|
@@ -14665,14 +15282,14 @@ function schemaForConfig(raw) {
|
|
|
14665
15282
|
var repairDeprecatedFields2 = repairDeprecatedFields;
|
|
14666
15283
|
var LEGACY_HEADER_ALIASES = /* @__PURE__ */ new Set(["version", "change_set_id"]);
|
|
14667
15284
|
var UNKNOWN_FIELD_CODE = "cli/unknown-field";
|
|
14668
|
-
function
|
|
15285
|
+
function isRecord5(value) {
|
|
14669
15286
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14670
15287
|
}
|
|
14671
15288
|
function unknownTopLevelKeys(raw) {
|
|
14672
|
-
if (!
|
|
15289
|
+
if (!isRecord5(raw)) return [];
|
|
14673
15290
|
const { schema } = schemaForConfig(raw);
|
|
14674
15291
|
const parsed = schema.safeParse(raw);
|
|
14675
|
-
if (!parsed.success || !
|
|
15292
|
+
if (!parsed.success || !isRecord5(parsed.data)) return [];
|
|
14676
15293
|
const recognized = new Set(Object.keys(parsed.data));
|
|
14677
15294
|
return Object.keys(raw).filter((key2) => !recognized.has(key2) && !LEGACY_HEADER_ALIASES.has(key2));
|
|
14678
15295
|
}
|
|
@@ -14916,17 +15533,17 @@ function finalOutputLines(params) {
|
|
|
14916
15533
|
|
|
14917
15534
|
// src/lib/handles-codegen.ts
|
|
14918
15535
|
var UNTYPED_GROUP = "untyped";
|
|
14919
|
-
function
|
|
15536
|
+
function isRecord6(value) {
|
|
14920
15537
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14921
15538
|
}
|
|
14922
15539
|
function nonEmptyString(value) {
|
|
14923
15540
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
14924
15541
|
}
|
|
14925
15542
|
function extractEntitlementHandles(config) {
|
|
14926
|
-
if (!
|
|
15543
|
+
if (!isRecord6(config) || !Array.isArray(config.entitlements)) return {};
|
|
14927
15544
|
const groups = /* @__PURE__ */ new Map();
|
|
14928
15545
|
for (const entry of config.entitlements) {
|
|
14929
|
-
if (!
|
|
15546
|
+
if (!isRecord6(entry)) continue;
|
|
14930
15547
|
const handle = nonEmptyString(entry.unique_handle) ?? nonEmptyString(entry.handle);
|
|
14931
15548
|
if (!handle) continue;
|
|
14932
15549
|
const type = nonEmptyString(entry.type) ?? UNTYPED_GROUP;
|
|
@@ -14996,36 +15613,36 @@ function collectStrings(values) {
|
|
|
14996
15613
|
return [...out].sort();
|
|
14997
15614
|
}
|
|
14998
15615
|
function extractPlanHandles(config) {
|
|
14999
|
-
if (!
|
|
15616
|
+
if (!isRecord6(config) || !Array.isArray(config.plans)) return [];
|
|
15000
15617
|
return collectStrings(
|
|
15001
15618
|
config.plans.map(
|
|
15002
|
-
(p) =>
|
|
15619
|
+
(p) => isRecord6(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
|
|
15003
15620
|
)
|
|
15004
15621
|
);
|
|
15005
15622
|
}
|
|
15006
15623
|
function extractSegmentHandles(config) {
|
|
15007
|
-
if (!
|
|
15624
|
+
if (!isRecord6(config) || !Array.isArray(config.segments)) return [];
|
|
15008
15625
|
return collectStrings(
|
|
15009
|
-
config.segments.map((s) =>
|
|
15626
|
+
config.segments.map((s) => isRecord6(s) ? nonEmptyString(s.handle) ?? s.id : null)
|
|
15010
15627
|
);
|
|
15011
15628
|
}
|
|
15012
15629
|
function extractSurfaceTemplateIds(config) {
|
|
15013
|
-
if (!
|
|
15630
|
+
if (!isRecord6(config)) return [];
|
|
15014
15631
|
const ids = [];
|
|
15015
15632
|
if (Array.isArray(config.surface_templates)) {
|
|
15016
15633
|
for (const t of config.surface_templates) {
|
|
15017
|
-
if (
|
|
15634
|
+
if (isRecord6(t)) ids.push(t.id ?? t.template_id);
|
|
15018
15635
|
}
|
|
15019
15636
|
}
|
|
15020
15637
|
const surfacesOf = (payload) => {
|
|
15021
|
-
if (!
|
|
15638
|
+
if (!isRecord6(payload) || !Array.isArray(payload.surfaces)) return;
|
|
15022
15639
|
for (const surface of payload.surfaces) {
|
|
15023
|
-
if (
|
|
15640
|
+
if (isRecord6(surface)) ids.push(surface.template_id);
|
|
15024
15641
|
}
|
|
15025
15642
|
};
|
|
15026
15643
|
if (Array.isArray(config.placements)) {
|
|
15027
15644
|
for (const placement of config.placements) {
|
|
15028
|
-
if (!
|
|
15645
|
+
if (!isRecord6(placement) || !Array.isArray(placement.payloads)) continue;
|
|
15029
15646
|
for (const payload of placement.payloads) surfacesOf(payload);
|
|
15030
15647
|
}
|
|
15031
15648
|
}
|
|
@@ -15035,9 +15652,9 @@ function extractSurfaceTemplateIds(config) {
|
|
|
15035
15652
|
return collectStrings(ids);
|
|
15036
15653
|
}
|
|
15037
15654
|
function extractUiPathActionTypes(config) {
|
|
15038
|
-
if (!
|
|
15655
|
+
if (!isRecord6(config) || !Array.isArray(config.content_ui_paths)) return [];
|
|
15039
15656
|
return collectStrings(
|
|
15040
|
-
config.content_ui_paths.map((p) =>
|
|
15657
|
+
config.content_ui_paths.map((p) => isRecord6(p) ? p.action_type : null)
|
|
15041
15658
|
);
|
|
15042
15659
|
}
|
|
15043
15660
|
function renderFlatFamily(lines, opts) {
|
|
@@ -15139,19 +15756,19 @@ function fail(cls, message) {
|
|
|
15139
15756
|
}
|
|
15140
15757
|
|
|
15141
15758
|
// src/lib/pin-drift.ts
|
|
15142
|
-
function
|
|
15759
|
+
function isRecord7(value) {
|
|
15143
15760
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15144
15761
|
}
|
|
15145
15762
|
function pinOf(pkg, name) {
|
|
15146
15763
|
for (const field of ["devDependencies", "dependencies"]) {
|
|
15147
15764
|
const deps = pkg[field];
|
|
15148
|
-
if (
|
|
15765
|
+
if (isRecord7(deps) && typeof deps[name] === "string") return deps[name];
|
|
15149
15766
|
}
|
|
15150
15767
|
return void 0;
|
|
15151
15768
|
}
|
|
15152
15769
|
var EXACT = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
15153
15770
|
function checkPinDrift(pkg) {
|
|
15154
|
-
if (!
|
|
15771
|
+
if (!isRecord7(pkg)) return [];
|
|
15155
15772
|
const warnings = [];
|
|
15156
15773
|
const cliPin = pinOf(pkg, "@revturbine/cli");
|
|
15157
15774
|
if (cliPin !== void 0 && !EXACT.test(cliPin)) {
|
|
@@ -16223,8 +16840,8 @@ program.command("preview").description("The open draft's Runtime Impact summary
|
|
|
16223
16840
|
emit(data, Boolean(opts.json), text);
|
|
16224
16841
|
});
|
|
16225
16842
|
program.command("evaluate").description(
|
|
16226
|
-
"Run placement/entitlement decisions for a user context against a config version \u2014 evaluated LOCALLY: the CLI fetches the version's Playbook and runs the SDK engine in-process (plan 192; clean-room, no suppression/cap history). Requires --live, --draft, or --release <id>."
|
|
16227
|
-
).option("--live", "Evaluate the live (deployed) configuration").option("--draft", "Evaluate the tenant's open draft").option("--release <id>", "Evaluate a past release (from its frozen snapshot)").option("--entitlement <handle>", "Check one entitlement (the checkEntitlement result)").option("--slot <id>", "Evaluate one surface slot (the getPlacement decision for that slot)").option("--component-type <type>", "Disambiguate a slot that can render more than one component, or resolve by component type alone").option("--surface-type <type>", "Deprecated alias for --component-type").option("--plan-handle <handle>", "Evaluate as if the user were on this plan (overrides the ctx file)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id,
|
|
16843
|
+
"Run placement/entitlement decisions for a user context against a config version \u2014 evaluated LOCALLY: the CLI fetches the version's Playbook and runs the SDK engine in-process (plan 192; clean-room, no suppression/cap history). The ctx file carries the full decision context \u2014 plan, traits, usage, trial state and a clock pin \u2014 so usage-threshold and trial placements decide here as they do in an app. Requires --live, --draft, or --release <id>."
|
|
16844
|
+
).option("--live", "Evaluate the live (deployed) configuration").option("--draft", "Evaluate the tenant's open draft").option("--release <id>", "Evaluate a past release (from its frozen snapshot)").option("--entitlement <handle>", "Check one entitlement (the checkEntitlement result)").option("--slot <id>", "Evaluate one surface slot (the getPlacement decision for that slot)").option("--component-type <type>", "Disambiguate a slot that can render more than one component, or resolve by component type alone").option("--surface-type <type>", "Deprecated alias for --component-type").option("--plan-handle <handle>", "Evaluate as if the user were on this plan (overrides the ctx file)").option("--usage <json>", `Usage entries keyed by entitlement handle, e.g. '{"api_calls":{"amount":95}}' (overrides the ctx file) \u2014 required to decide usage/credit-threshold placements`).option("--trial-status <json>", `Trial status, e.g. '{"in_trial":true,"trial_limit_type":"time","days_remaining":3}' (overrides the ctx file) \u2014 required to decide trial placements`).option("--now <iso>", "Pin the clock (ISO-8601) for trial derivation from the ctx file's trial_instances (overrides ctx now_iso)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id, plan_handle?, traits?, usage?, trial?, trial_instances?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
|
|
16228
16845
|
async (opts) => {
|
|
16229
16846
|
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
|
|
16230
16847
|
const conn = connect(opts.url, opts.tenantId);
|
|
@@ -16242,8 +16859,18 @@ program.command("evaluate").description(
|
|
|
16242
16859
|
}
|
|
16243
16860
|
const userId = typeof ctx.user_id === "string" && ctx.user_id.length > 0 ? ctx.user_id : void 0;
|
|
16244
16861
|
if (!userId) fail(EXIT.VALIDATION, `user file must carry a non-empty string "user_id" (${opts.user})`);
|
|
16245
|
-
|
|
16246
|
-
|
|
16862
|
+
const parsed = parseDecisionContext({
|
|
16863
|
+
ctx,
|
|
16864
|
+
usage: opts.usage,
|
|
16865
|
+
trialStatus: opts.trialStatus,
|
|
16866
|
+
now: opts.now
|
|
16867
|
+
});
|
|
16868
|
+
if (!parsed.ok) fail(EXIT.VALIDATION, `invalid decision context in ${opts.user}: ${parsed.errors.join("; ")}`);
|
|
16869
|
+
const decisionContext = parsed.context;
|
|
16870
|
+
if (decisionContext.nowIso && !decisionContext.trialInstances) {
|
|
16871
|
+
diag(
|
|
16872
|
+
'NOTE: the pinned clock only moves trial derivation from trial_instances; an explicit "trial" status is already fixed in time.'
|
|
16873
|
+
);
|
|
16247
16874
|
}
|
|
16248
16875
|
if (typeof ctx.customer_id === "string") {
|
|
16249
16876
|
diag(
|
|
@@ -16262,6 +16889,7 @@ program.command("evaluate").description(
|
|
|
16262
16889
|
userId,
|
|
16263
16890
|
planHandle,
|
|
16264
16891
|
traits,
|
|
16892
|
+
...decisionContext,
|
|
16265
16893
|
entitlementHandles: opts.entitlement ? [opts.entitlement] : bulk ? asStringArray2(ctx.entitlement_handles) : [],
|
|
16266
16894
|
placementIds: bulk ? asStringArray2(ctx.placement_ids) : [],
|
|
16267
16895
|
slot: wantsSlot ? { slotId: opts.slot, componentType } : void 0
|
|
@@ -16271,10 +16899,18 @@ program.command("evaluate").description(
|
|
|
16271
16899
|
tenant_id: conn.tenantId,
|
|
16272
16900
|
user_id: userId,
|
|
16273
16901
|
...playbookVersionId ? { playbook_version_id: playbookVersionId } : {},
|
|
16274
|
-
|
|
16902
|
+
// The pinned clock when one was given, so the stamp matches the
|
|
16903
|
+
// instant the decisions were made against.
|
|
16904
|
+
evaluated_at: decisionContext.nowIso ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
16275
16905
|
// Provenance: decided in-process by the SDK engine, not by a server.
|
|
16276
16906
|
evaluation: "local",
|
|
16277
|
-
|
|
16907
|
+
decisions: result.decisions,
|
|
16908
|
+
// Which authored payload the resolver chose, per requested placement.
|
|
16909
|
+
payload_ids: result.payloadIds,
|
|
16910
|
+
entitlements: result.entitlements,
|
|
16911
|
+
placement: result.placement,
|
|
16912
|
+
personalization_tokens: result.personalizationTokens,
|
|
16913
|
+
...result.trialStatus !== void 0 ? { trial_status: result.trialStatus } : {}
|
|
16278
16914
|
},
|
|
16279
16915
|
true
|
|
16280
16916
|
);
|
|
@@ -16546,10 +17182,23 @@ var COMMAND_EXAMPLES = {
|
|
|
16546
17182
|
].join("\n"),
|
|
16547
17183
|
evaluate: [
|
|
16548
17184
|
"",
|
|
16549
|
-
"
|
|
17185
|
+
"Examples:",
|
|
16550
17186
|
" revturbine evaluate --live --user ./ctx.json --entitlement seats",
|
|
16551
17187
|
" revturbine evaluate --draft --user ./ctx.json --slot upgrade_banner --component-type banner --plan-handle pro",
|
|
16552
|
-
|
|
17188
|
+
` revturbine evaluate --live --user ./ctx.json --usage '{"api_calls":{"amount":95}}'`,
|
|
17189
|
+
` revturbine evaluate --live --user ./ctx.json --trial-status '{"in_trial":true,"trial_limit_type":"time","days_remaining":3}'`,
|
|
17190
|
+
" revturbine evaluate --live --user ./ctx.json --now 2026-09-21T00:00:00Z",
|
|
17191
|
+
"",
|
|
17192
|
+
"ctx.json \u2014 the whole decision context:",
|
|
17193
|
+
' { "user_id": "u1", "plan_handle": "pro", "traits": { "tier": "smb" },',
|
|
17194
|
+
' "usage": { "api_calls": { "amount": 95, "limit": 100 } },',
|
|
17195
|
+
' "trial": { "in_trial": true, "trial_limit_type": "time", "days_remaining": 3 },',
|
|
17196
|
+
' "trial_instances": [ ... ], "now_iso": "2026-09-21T00:00:00Z",',
|
|
17197
|
+
' "entitlement_handles": ["seats"], "placement_ids": ["pl_usage_warn"] }',
|
|
17198
|
+
"",
|
|
17199
|
+
` usage \u2014 omit "limit" and it is taken from the Playbook's entitlement rule.`,
|
|
17200
|
+
" trial \u2014 an explicit status; trial_instances + now_iso derive one at that clock.",
|
|
17201
|
+
" Output carries payload_ids: which authored payload won, per placement."
|
|
16553
17202
|
].join("\n")
|
|
16554
17203
|
};
|
|
16555
17204
|
for (const [name, text] of Object.entries(COMMAND_EXAMPLES)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.4",
|
|
4
4
|
"description": "revturbine — validate RevTurbine Playbooks and ship them to a RevTurbine instance through the playbook-version lifecycle (draft → Release).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify": "npm run typecheck && npm run lint && npm run check:schema && npm run test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@revturbine/sdk": "
|
|
33
|
+
"@revturbine/sdk": ">=0.10.9 <1",
|
|
34
34
|
"commander": "^15.0.0",
|
|
35
35
|
"semver": "^7.8.5",
|
|
36
36
|
"zod": "4.4.3"
|