@revturbine/cli 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +223 -31
  3. 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
@@ -14390,28 +14390,181 @@ function formatAuthentication(hasToken, tokenValid) {
14390
14390
  return "authentication: verification unavailable \u2014 credentials present; retry when the instance is reachable";
14391
14391
  }
14392
14392
 
14393
+ // src/lib/evaluate-context.ts
14394
+ function isRecord4(value) {
14395
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14396
+ }
14397
+ var DEFAULT_USAGE_UNIT = "unit";
14398
+ function normalizeUsageEntry(handle, value, errors) {
14399
+ if (typeof value === "number") {
14400
+ if (!Number.isFinite(value) || value < 0) {
14401
+ errors.push(`usage.${handle} must be a finite number \u2265 0`);
14402
+ return void 0;
14403
+ }
14404
+ return { entitlement_handle: handle, unit: DEFAULT_USAGE_UNIT, amount: value };
14405
+ }
14406
+ if (!isRecord4(value)) {
14407
+ errors.push(`usage.${handle} must be a number or an object with "amount"`);
14408
+ return void 0;
14409
+ }
14410
+ const amount = value.amount;
14411
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
14412
+ errors.push(`usage.${handle}.amount must be a finite number \u2265 0`);
14413
+ return void 0;
14414
+ }
14415
+ const entry = {
14416
+ entitlement_handle: typeof value.entitlement_handle === "string" ? value.entitlement_handle : handle,
14417
+ unit: typeof value.unit === "string" && value.unit.length > 0 ? value.unit : DEFAULT_USAGE_UNIT,
14418
+ amount
14419
+ };
14420
+ if (value.limit !== void 0) {
14421
+ if (typeof value.limit !== "number" || !Number.isFinite(value.limit) || value.limit < 0) {
14422
+ errors.push(`usage.${handle}.limit must be a finite number \u2265 0`);
14423
+ } else {
14424
+ entry.limit = value.limit;
14425
+ }
14426
+ }
14427
+ if (value.reset_date !== void 0) {
14428
+ if (typeof value.reset_date !== "string") {
14429
+ errors.push(`usage.${handle}.reset_date must be a string`);
14430
+ } else {
14431
+ entry.reset_date = value.reset_date;
14432
+ }
14433
+ }
14434
+ return entry;
14435
+ }
14436
+ function normalizeUsage(value, errors) {
14437
+ if (value === void 0 || value === null) return void 0;
14438
+ if (!isRecord4(value)) {
14439
+ errors.push("usage must be an object keyed by entitlement handle");
14440
+ return void 0;
14441
+ }
14442
+ const out = {};
14443
+ for (const [handle, raw] of Object.entries(value)) {
14444
+ const entry = normalizeUsageEntry(handle, raw, errors);
14445
+ if (entry) out[handle] = entry;
14446
+ }
14447
+ return Object.keys(out).length > 0 ? out : void 0;
14448
+ }
14449
+ function normalizeTrialStatus(value, errors) {
14450
+ if (value === void 0 || value === null) return void 0;
14451
+ if (!isRecord4(value)) {
14452
+ errors.push("trial status must be an object (the SDK's UserTrialStatus)");
14453
+ return void 0;
14454
+ }
14455
+ if (typeof value.in_trial !== "boolean") {
14456
+ errors.push('trial status must carry a boolean "in_trial"');
14457
+ return void 0;
14458
+ }
14459
+ return value;
14460
+ }
14461
+ function normalizeTrialInstances(value, errors) {
14462
+ if (value === void 0 || value === null) return void 0;
14463
+ if (!Array.isArray(value) || value.some((item) => !isRecord4(item))) {
14464
+ errors.push("trial_instances must be an array of TrialInstance objects");
14465
+ return void 0;
14466
+ }
14467
+ return value.length > 0 ? value : void 0;
14468
+ }
14469
+ function normalizeNowIso(value, source, errors) {
14470
+ if (value === void 0 || value === null) return void 0;
14471
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
14472
+ errors.push(`${source} must be an ISO-8601 timestamp (e.g. 2026-09-21T00:00:00Z)`);
14473
+ return void 0;
14474
+ }
14475
+ return new Date(value).toISOString();
14476
+ }
14477
+ function parseJsonFlag(raw, flag, errors) {
14478
+ try {
14479
+ return JSON.parse(raw);
14480
+ } catch (err) {
14481
+ errors.push(`${flag} must be valid JSON: ${err.message}`);
14482
+ return void 0;
14483
+ }
14484
+ }
14485
+ function parseDecisionContext(input) {
14486
+ const errors = [];
14487
+ const usageSource = input.usage !== void 0 ? parseJsonFlag(input.usage, "--usage", errors) : input.ctx.usage;
14488
+ const usage = normalizeUsage(usageSource, errors);
14489
+ const trialSource = input.trialStatus !== void 0 ? parseJsonFlag(input.trialStatus, "--trial-status", errors) : input.ctx.trial;
14490
+ const trialStatus = normalizeTrialStatus(trialSource, errors);
14491
+ const trialInstances = normalizeTrialInstances(input.ctx.trial_instances, errors);
14492
+ const nowIso = input.now !== void 0 ? normalizeNowIso(input.now, "--now", errors) : normalizeNowIso(input.ctx.now_iso, "now_iso", errors);
14493
+ if (errors.length > 0) return { ok: false, errors };
14494
+ return {
14495
+ ok: true,
14496
+ context: {
14497
+ ...usage ? { usage } : {},
14498
+ ...trialStatus ? { trialStatus } : {},
14499
+ ...trialInstances ? { trialInstances } : {},
14500
+ ...nowIso ? { nowIso } : {}
14501
+ }
14502
+ };
14503
+ }
14504
+
14393
14505
  // src/lib/evaluate-local.ts
14394
14506
  import { initRevTurbine } from "@revturbine/sdk/headless";
14395
14507
  function resolvePlacementComponentType(input) {
14396
14508
  return input.componentType ?? input.surfaceType;
14397
14509
  }
14510
+ function payloadIdOf(decision) {
14511
+ if (typeof decision !== "object" || decision === null) return null;
14512
+ const output = decision.output;
14513
+ if (typeof output !== "object" || output === null) return null;
14514
+ const id = output.output_id;
14515
+ return typeof id === "string" ? id : null;
14516
+ }
14517
+ async function completeUsageLimits(session, usage) {
14518
+ let completed = false;
14519
+ const filled = {};
14520
+ for (const [handle, entry] of Object.entries(usage)) {
14521
+ if (entry.limit !== void 0) {
14522
+ filled[handle] = entry;
14523
+ continue;
14524
+ }
14525
+ const result = await session.checkEntitlement(entry.entitlement_handle);
14526
+ if (typeof result?.limit === "number" && Number.isFinite(result.limit)) {
14527
+ filled[handle] = { ...entry, limit: result.limit };
14528
+ completed = true;
14529
+ } else {
14530
+ filled[handle] = entry;
14531
+ }
14532
+ }
14533
+ if (completed) session.setUserContext({ usage: filled });
14534
+ }
14398
14535
  async function evaluateLocal(config, input) {
14399
14536
  const user = { id: input.userId };
14400
14537
  if (input.planHandle) user.plan_handle = input.planHandle;
14401
14538
  if (input.traits && Object.keys(input.traits).length > 0) user.custom = input.traits;
14539
+ if (input.usage) user.usage = input.usage;
14402
14540
  const session = await initRevTurbine({
14403
14541
  user,
14404
- localRuntime: { playbook: config }
14542
+ localRuntime: {
14543
+ playbook: config,
14544
+ ...input.trialStatus ? { initialData: { trialStatus: input.trialStatus } } : {}
14545
+ }
14405
14546
  });
14547
+ if (input.trialInstances) {
14548
+ await session.sdk.setTrialInstances(
14549
+ input.trialInstances,
14550
+ input.nowIso ? { nowIso: input.nowIso } : void 0
14551
+ );
14552
+ }
14553
+ const trialAsked = Boolean(input.trialInstances || input.trialStatus);
14554
+ const trialStatus = trialAsked ? await session.getTrialStatus() : void 0;
14555
+ if (input.usage) await completeUsageLimits(session, input.usage);
14406
14556
  const entitlements = {};
14407
14557
  for (const handle of input.entitlementHandles ?? []) {
14408
14558
  entitlements[handle] = await session.checkEntitlement(handle);
14409
14559
  }
14410
14560
  const decisions = [];
14561
+ const payloadIds = {};
14411
14562
  for (const placementId of input.placementIds ?? []) {
14412
14563
  const controller = session.placement({ placement: { name: placementId } });
14413
14564
  await controller.load();
14414
- decisions.push(controller.state.decision);
14565
+ const decision = controller.state.decision;
14566
+ decisions.push(decision);
14567
+ payloadIds[placementId] = payloadIdOf(decision);
14415
14568
  }
14416
14569
  const componentType = input.slot ? resolvePlacementComponentType(input.slot) : void 0;
14417
14570
  const placement = input.slot && (input.slot.slotId || componentType) ? await session.getPlacement({
@@ -14419,7 +14572,14 @@ async function evaluateLocal(config, input) {
14419
14572
  ...componentType ? { componentType } : {},
14420
14573
  ...input.planHandle ? { planHandle: input.planHandle } : {}
14421
14574
  }) : null;
14422
- return { decisions, entitlements, placement };
14575
+ return {
14576
+ decisions,
14577
+ payloadIds,
14578
+ entitlements,
14579
+ placement,
14580
+ personalizationTokens: session.sdk.getPersonalizationTokens(),
14581
+ ...trialStatus !== void 0 ? { trialStatus } : {}
14582
+ };
14423
14583
  }
14424
14584
 
14425
14585
  // src/lib/ingest-keys.ts
@@ -14665,14 +14825,14 @@ function schemaForConfig(raw) {
14665
14825
  var repairDeprecatedFields2 = repairDeprecatedFields;
14666
14826
  var LEGACY_HEADER_ALIASES = /* @__PURE__ */ new Set(["version", "change_set_id"]);
14667
14827
  var UNKNOWN_FIELD_CODE = "cli/unknown-field";
14668
- function isRecord4(value) {
14828
+ function isRecord5(value) {
14669
14829
  return typeof value === "object" && value !== null && !Array.isArray(value);
14670
14830
  }
14671
14831
  function unknownTopLevelKeys(raw) {
14672
- if (!isRecord4(raw)) return [];
14832
+ if (!isRecord5(raw)) return [];
14673
14833
  const { schema } = schemaForConfig(raw);
14674
14834
  const parsed = schema.safeParse(raw);
14675
- if (!parsed.success || !isRecord4(parsed.data)) return [];
14835
+ if (!parsed.success || !isRecord5(parsed.data)) return [];
14676
14836
  const recognized = new Set(Object.keys(parsed.data));
14677
14837
  return Object.keys(raw).filter((key2) => !recognized.has(key2) && !LEGACY_HEADER_ALIASES.has(key2));
14678
14838
  }
@@ -14916,17 +15076,17 @@ function finalOutputLines(params) {
14916
15076
 
14917
15077
  // src/lib/handles-codegen.ts
14918
15078
  var UNTYPED_GROUP = "untyped";
14919
- function isRecord5(value) {
15079
+ function isRecord6(value) {
14920
15080
  return typeof value === "object" && value !== null && !Array.isArray(value);
14921
15081
  }
14922
15082
  function nonEmptyString(value) {
14923
15083
  return typeof value === "string" && value.length > 0 ? value : null;
14924
15084
  }
14925
15085
  function extractEntitlementHandles(config) {
14926
- if (!isRecord5(config) || !Array.isArray(config.entitlements)) return {};
15086
+ if (!isRecord6(config) || !Array.isArray(config.entitlements)) return {};
14927
15087
  const groups = /* @__PURE__ */ new Map();
14928
15088
  for (const entry of config.entitlements) {
14929
- if (!isRecord5(entry)) continue;
15089
+ if (!isRecord6(entry)) continue;
14930
15090
  const handle = nonEmptyString(entry.unique_handle) ?? nonEmptyString(entry.handle);
14931
15091
  if (!handle) continue;
14932
15092
  const type = nonEmptyString(entry.type) ?? UNTYPED_GROUP;
@@ -14996,36 +15156,36 @@ function collectStrings(values) {
14996
15156
  return [...out].sort();
14997
15157
  }
14998
15158
  function extractPlanHandles(config) {
14999
- if (!isRecord5(config) || !Array.isArray(config.plans)) return [];
15159
+ if (!isRecord6(config) || !Array.isArray(config.plans)) return [];
15000
15160
  return collectStrings(
15001
15161
  config.plans.map(
15002
- (p) => isRecord5(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
15162
+ (p) => isRecord6(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
15003
15163
  )
15004
15164
  );
15005
15165
  }
15006
15166
  function extractSegmentHandles(config) {
15007
- if (!isRecord5(config) || !Array.isArray(config.segments)) return [];
15167
+ if (!isRecord6(config) || !Array.isArray(config.segments)) return [];
15008
15168
  return collectStrings(
15009
- config.segments.map((s) => isRecord5(s) ? nonEmptyString(s.handle) ?? s.id : null)
15169
+ config.segments.map((s) => isRecord6(s) ? nonEmptyString(s.handle) ?? s.id : null)
15010
15170
  );
15011
15171
  }
15012
15172
  function extractSurfaceTemplateIds(config) {
15013
- if (!isRecord5(config)) return [];
15173
+ if (!isRecord6(config)) return [];
15014
15174
  const ids = [];
15015
15175
  if (Array.isArray(config.surface_templates)) {
15016
15176
  for (const t of config.surface_templates) {
15017
- if (isRecord5(t)) ids.push(t.id ?? t.template_id);
15177
+ if (isRecord6(t)) ids.push(t.id ?? t.template_id);
15018
15178
  }
15019
15179
  }
15020
15180
  const surfacesOf = (payload) => {
15021
- if (!isRecord5(payload) || !Array.isArray(payload.surfaces)) return;
15181
+ if (!isRecord6(payload) || !Array.isArray(payload.surfaces)) return;
15022
15182
  for (const surface of payload.surfaces) {
15023
- if (isRecord5(surface)) ids.push(surface.template_id);
15183
+ if (isRecord6(surface)) ids.push(surface.template_id);
15024
15184
  }
15025
15185
  };
15026
15186
  if (Array.isArray(config.placements)) {
15027
15187
  for (const placement of config.placements) {
15028
- if (!isRecord5(placement) || !Array.isArray(placement.payloads)) continue;
15188
+ if (!isRecord6(placement) || !Array.isArray(placement.payloads)) continue;
15029
15189
  for (const payload of placement.payloads) surfacesOf(payload);
15030
15190
  }
15031
15191
  }
@@ -15035,9 +15195,9 @@ function extractSurfaceTemplateIds(config) {
15035
15195
  return collectStrings(ids);
15036
15196
  }
15037
15197
  function extractUiPathActionTypes(config) {
15038
- if (!isRecord5(config) || !Array.isArray(config.content_ui_paths)) return [];
15198
+ if (!isRecord6(config) || !Array.isArray(config.content_ui_paths)) return [];
15039
15199
  return collectStrings(
15040
- config.content_ui_paths.map((p) => isRecord5(p) ? p.action_type : null)
15200
+ config.content_ui_paths.map((p) => isRecord6(p) ? p.action_type : null)
15041
15201
  );
15042
15202
  }
15043
15203
  function renderFlatFamily(lines, opts) {
@@ -15139,19 +15299,19 @@ function fail(cls, message) {
15139
15299
  }
15140
15300
 
15141
15301
  // src/lib/pin-drift.ts
15142
- function isRecord6(value) {
15302
+ function isRecord7(value) {
15143
15303
  return typeof value === "object" && value !== null && !Array.isArray(value);
15144
15304
  }
15145
15305
  function pinOf(pkg, name) {
15146
15306
  for (const field of ["devDependencies", "dependencies"]) {
15147
15307
  const deps = pkg[field];
15148
- if (isRecord6(deps) && typeof deps[name] === "string") return deps[name];
15308
+ if (isRecord7(deps) && typeof deps[name] === "string") return deps[name];
15149
15309
  }
15150
15310
  return void 0;
15151
15311
  }
15152
15312
  var EXACT = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
15153
15313
  function checkPinDrift(pkg) {
15154
- if (!isRecord6(pkg)) return [];
15314
+ if (!isRecord7(pkg)) return [];
15155
15315
  const warnings = [];
15156
15316
  const cliPin = pinOf(pkg, "@revturbine/cli");
15157
15317
  if (cliPin !== void 0 && !EXACT.test(cliPin)) {
@@ -16223,8 +16383,8 @@ program.command("preview").description("The open draft's Runtime Impact summary
16223
16383
  emit(data, Boolean(opts.json), text);
16224
16384
  });
16225
16385
  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, customer_id?, plan_handle?, traits?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
16386
+ "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>."
16387
+ ).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
16388
  async (opts) => {
16229
16389
  const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
16230
16390
  const conn = connect(opts.url, opts.tenantId);
@@ -16242,8 +16402,18 @@ program.command("evaluate").description(
16242
16402
  }
16243
16403
  const userId = typeof ctx.user_id === "string" && ctx.user_id.length > 0 ? ctx.user_id : void 0;
16244
16404
  if (!userId) fail(EXIT.VALIDATION, `user file must carry a non-empty string "user_id" (${opts.user})`);
16245
- if (typeof ctx.now_iso === "string") {
16246
- diag("WARNING: now_iso is ignored \u2014 local evaluation uses the real clock.");
16405
+ const parsed = parseDecisionContext({
16406
+ ctx,
16407
+ usage: opts.usage,
16408
+ trialStatus: opts.trialStatus,
16409
+ now: opts.now
16410
+ });
16411
+ if (!parsed.ok) fail(EXIT.VALIDATION, `invalid decision context in ${opts.user}: ${parsed.errors.join("; ")}`);
16412
+ const decisionContext = parsed.context;
16413
+ if (decisionContext.nowIso && !decisionContext.trialInstances) {
16414
+ diag(
16415
+ 'NOTE: the pinned clock only moves trial derivation from trial_instances; an explicit "trial" status is already fixed in time.'
16416
+ );
16247
16417
  }
16248
16418
  if (typeof ctx.customer_id === "string") {
16249
16419
  diag(
@@ -16262,6 +16432,7 @@ program.command("evaluate").description(
16262
16432
  userId,
16263
16433
  planHandle,
16264
16434
  traits,
16435
+ ...decisionContext,
16265
16436
  entitlementHandles: opts.entitlement ? [opts.entitlement] : bulk ? asStringArray2(ctx.entitlement_handles) : [],
16266
16437
  placementIds: bulk ? asStringArray2(ctx.placement_ids) : [],
16267
16438
  slot: wantsSlot ? { slotId: opts.slot, componentType } : void 0
@@ -16271,10 +16442,18 @@ program.command("evaluate").description(
16271
16442
  tenant_id: conn.tenantId,
16272
16443
  user_id: userId,
16273
16444
  ...playbookVersionId ? { playbook_version_id: playbookVersionId } : {},
16274
- evaluated_at: (/* @__PURE__ */ new Date()).toISOString(),
16445
+ // The pinned clock when one was given, so the stamp matches the
16446
+ // instant the decisions were made against.
16447
+ evaluated_at: decisionContext.nowIso ?? (/* @__PURE__ */ new Date()).toISOString(),
16275
16448
  // Provenance: decided in-process by the SDK engine, not by a server.
16276
16449
  evaluation: "local",
16277
- ...result
16450
+ decisions: result.decisions,
16451
+ // Which authored payload the resolver chose, per requested placement.
16452
+ payload_ids: result.payloadIds,
16453
+ entitlements: result.entitlements,
16454
+ placement: result.placement,
16455
+ personalization_tokens: result.personalizationTokens,
16456
+ ...result.trialStatus !== void 0 ? { trial_status: result.trialStatus } : {}
16278
16457
  },
16279
16458
  true
16280
16459
  );
@@ -16546,10 +16725,23 @@ var COMMAND_EXAMPLES = {
16546
16725
  ].join("\n"),
16547
16726
  evaluate: [
16548
16727
  "",
16549
- "Example:",
16728
+ "Examples:",
16550
16729
  " revturbine evaluate --live --user ./ctx.json --entitlement seats",
16551
16730
  " revturbine evaluate --draft --user ./ctx.json --slot upgrade_banner --component-type banner --plan-handle pro",
16552
- ' ctx.json: { "user_id": "u1", "plan_handle": "pro", "entitlement_handles": ["seats"] }'
16731
+ ` revturbine evaluate --live --user ./ctx.json --usage '{"api_calls":{"amount":95}}'`,
16732
+ ` revturbine evaluate --live --user ./ctx.json --trial-status '{"in_trial":true,"trial_limit_type":"time","days_remaining":3}'`,
16733
+ " revturbine evaluate --live --user ./ctx.json --now 2026-09-21T00:00:00Z",
16734
+ "",
16735
+ "ctx.json \u2014 the whole decision context:",
16736
+ ' { "user_id": "u1", "plan_handle": "pro", "traits": { "tier": "smb" },',
16737
+ ' "usage": { "api_calls": { "amount": 95, "limit": 100 } },',
16738
+ ' "trial": { "in_trial": true, "trial_limit_type": "time", "days_remaining": 3 },',
16739
+ ' "trial_instances": [ ... ], "now_iso": "2026-09-21T00:00:00Z",',
16740
+ ' "entitlement_handles": ["seats"], "placement_ids": ["pl_usage_warn"] }',
16741
+ "",
16742
+ ` usage \u2014 omit "limit" and it is taken from the Playbook's entitlement rule.`,
16743
+ " trial \u2014 an explicit status; trial_instances + now_iso derive one at that clock.",
16744
+ " Output carries payload_ids: which authored payload won, per placement."
16553
16745
  ].join("\n")
16554
16746
  };
16555
16747
  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.2",
3
+ "version": "0.19.3",
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": "^0.7.4",
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"