@configdirector/cli 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.22.0] - 2026-11-11
8
+
9
+ ### Added
10
+
11
+ - `configdirector configs runs` shows how long a run in progress needs, under each rollout's primary metric results once its variations can be compared. For a real lift of 2%, 5%, 10%, 20% and 50% on the primary metric, in its better direction, it gives the exposures so far against those needed before a variation with that lift has a good chance of reading likely better, and about how many days remain at the pace of new exposures since the time it names. A lift the rollout has enough exposures for says so, more than 365 days reads as more than a year, and when no new contexts were exposed since that time, it says the days cannot be estimated and gives the exposures alone. It also says that a row's lift is one a variation might really have, not the lift measured so far, and that whatever the estimate says, a run should cover at least one full week. The estimate doesn't show while the control's numbers give nothing to estimate from, as when none of its contexts has sent the event yet. `--json` prints the Admin API's response as received, as before.
12
+
7
13
  ## [0.21.0] - 2026-11-10
8
14
 
9
15
  ### Added
package/README.md CHANGED
@@ -342,7 +342,15 @@ Running since 2026-11-03 12:00 UTC
342
342
  810 contexts exposed, 3201 evaluations. The split matches the configured shares.
343
343
  Primary metric: checkout_completed, conversion, higher is better
344
344
  control (Start free trial) 2.9% control
345
- treatment-a (Try it free for 14 days) 4.5% +55% (+12% to +98%) >99% chance to beat control likely better
345
+ treatment-a (Try it free for 14 days) 3.5% +19% (-43% to +154%) 69% chance to beat control no clear difference
346
+ How long to run, at the pace since 2026-11-04 00:00 UTC: about 98 new exposures a day
347
+ For a real lift of +2% 810 of about 2,082,762 exposures more than a year remaining
348
+ For a real lift of +5% 810 of about 338,034 exposures more than a year remaining
349
+ For a real lift of +10% 810 of about 86,501 exposures more than a year remaining
350
+ For a real lift of +20% 810 of about 22,617 exposures about 222 days remaining
351
+ For a real lift of +50% 810 of about 4,089 exposures about 34 days remaining
352
+ A row's lift is one a variation might really have over the control, not the lift measured so far: the smaller it is, the longer the run.
353
+ Whatever the estimate says, a run should cover at least one full week, so every day of the week is in the result.
346
354
  Guardrail: support_ticket_opened, count per context, lower is better
347
355
  control (Start free trial) 0.12 control
348
356
  treatment-a (Try it free for 14 days) 0.13 +8% (-10% to +29%) 22% chance to beat control no clear difference
@@ -350,7 +358,7 @@ Running since 2026-11-03 12:00 UTC
350
358
  Results can be up to 25 minutes behind.
351
359
  ```
352
360
 
353
- Each kind a metric reports gets a line with the metric's role, its event name, the kind and its direction, then one row per variation, the control first: its value, and for every other variation the lift against the control with the range it is likely in, the chance to beat control and the verdict, `likely better`, `likely worse` or `no clear difference`. With three or more variations, each row also shows the variation's chance to be best. Until every variation has enough exposures, the rollout says the run needs more exposures and shows the values alone. An ended run's last line says when its final results were taken, once they are in. A run also shows its hypothesis and the note it ended with, and lists the events sent with `track` during the run that no metric measures.
361
+ Each kind a metric reports gets a line with the metric's role, its event name, the kind and its direction, then one row per variation, the control first: its value, and for every other variation the lift against the control with the range it is likely in, the chance to beat control and the verdict, `likely better`, `likely worse` or `no clear difference`. With three or more variations, each row also shows the variation's chance to be best. Until every variation has enough exposures, the rollout says the run needs more exposures and shows the values alone. While a run is going, `How long to run` follows the primary metric's results: for a real lift of 2%, 5%, 10%, 20% and 50%, in the metric's better direction, the exposures so far against those needed before a variation with that lift has a good chance of reading likely better, and about how many days remain at the pace of new exposures since the time it names. An ended run's last line says when its final results were taken, once they are in. A run also shows its hypothesis and the note it ended with, and lists the events sent with `track` during the run that no metric measures.
354
362
 
355
363
  - `configs ship` serves one variation to everyone in the environment and ends the run. `--variation` takes its name or its ID. The default targeting rule value becomes the variation's value, every percentage rollout is removed, including one a conditional rule serves, and a conditional rule serving a single value stays.
356
364
  - `configs stop` ends the run without choosing a variation: every percentage rollout is removed, including one a conditional rule serves, and the default targeting rule value and any conditional rule serving a single value stay.
@@ -14,7 +14,7 @@ import { promisify } from "node:util";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import { text } from "node:stream/consumers";
16
16
  //#region package.json
17
- var version = "0.21.0";
17
+ var version = "0.22.0";
18
18
  //#endregion
19
19
  //#region src/CliError.ts
20
20
  var CliError = class extends Error {
@@ -146,11 +146,22 @@ const VariationResultSchema = v.object({
146
146
  chanceToBeBest: v.optional(v.number()),
147
147
  verdict: v.optional(v.string())
148
148
  });
149
+ const RunLengthEstimateSchema = v.object({
150
+ analyzedExposures: v.number(),
151
+ exposuresPerDay: v.number(),
152
+ paceMeasuredSince: TimestampSchema,
153
+ lifts: v.array(v.object({
154
+ lift: v.number(),
155
+ exposuresNeeded: v.number(),
156
+ daysRemaining: v.optional(v.number())
157
+ }))
158
+ });
149
159
  const MetricKindResultSchema = v.object({
150
160
  kind: v.string(),
151
161
  rollouts: v.array(v.object({
152
162
  ruleId: v.string(),
153
- variations: v.array(VariationResultSchema)
163
+ variations: v.array(VariationResultSchema),
164
+ runLengthEstimate: v.optional(RunLengthEstimateSchema)
154
165
  }))
155
166
  });
156
167
  const MetricResultSchema = v.object({
@@ -1676,9 +1687,53 @@ const resultRow = (variation, table) => {
1676
1687
  }
1677
1688
  };
1678
1689
  const capitalized = (text) => `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
1690
+ const DAYS_IN_A_YEAR = 365;
1691
+ const formatCount = (count) => count.toLocaleString("en-US");
1692
+ const liftRowFormOf = (daysRemaining) => {
1693
+ if (daysRemaining === void 0) return { form: "no-pace" };
1694
+ if (daysRemaining === 0) return { form: "enough-exposures" };
1695
+ return daysRemaining > DAYS_IN_A_YEAR ? { form: "more-than-a-year" } : {
1696
+ form: "days-remaining",
1697
+ days: daysRemaining
1698
+ };
1699
+ };
1700
+ const daysRemainingCells = (liftRow) => {
1701
+ switch (liftRow.form) {
1702
+ case "days-remaining": return [`about ${formatCount(liftRow.days)} ${plural(liftRow.days, "day", "days")} remaining`];
1703
+ case "more-than-a-year": return ["more than a year remaining"];
1704
+ case "enough-exposures": return ["enough exposures"];
1705
+ case "no-pace": return [];
1706
+ }
1707
+ };
1708
+ const exposurePaceOf = (estimate) => estimate.exposuresPerDay > 0 ? {
1709
+ form: "new-exposures",
1710
+ exposuresPerDay: Math.round(estimate.exposuresPerDay)
1711
+ } : { form: "no-new-exposures" };
1712
+ const paceLine = (estimate) => {
1713
+ const since = utcMinute(estimate.paceMeasuredSince);
1714
+ const pace = exposurePaceOf(estimate);
1715
+ switch (pace.form) {
1716
+ case "new-exposures": return `How long to run, at the pace since ${since}: about ${formatCount(pace.exposuresPerDay)} ${plural(pace.exposuresPerDay, "new exposure", "new exposures")} a day`;
1717
+ case "no-new-exposures": return `How long to run: no new contexts were exposed since ${since}, so the days cannot be estimated.`;
1718
+ }
1719
+ };
1720
+ const runLengthEstimateLines = (estimate) => {
1721
+ if (estimate === void 0) return [];
1722
+ return [
1723
+ paceLine(estimate),
1724
+ ...alignedColumns(estimate.lifts.map((liftEstimate) => [
1725
+ `For a real lift of ${formatLift(liftEstimate.lift)}`,
1726
+ `${formatCount(estimate.analyzedExposures)} of about ${formatCount(liftEstimate.exposuresNeeded)} exposures`,
1727
+ ...daysRemainingCells(liftRowFormOf(liftEstimate.daysRemaining))
1728
+ ])).map((line) => ` ${line}`),
1729
+ "A row's lift is one a variation might really have over the control, not the lift measured so far: the smaller it is, the longer the run.",
1730
+ "Whatever the estimate says, a run should cover at least one full week, so every day of the week is in the result."
1731
+ ];
1732
+ };
1679
1733
  const kindResultLines = (role, metric, kindResult, rollout, variations) => {
1680
- const variationResults = kindResult.rollouts.find((candidate) => candidate.ruleId === rollout.ruleId)?.variations;
1681
- if (variationResults === void 0) return [];
1734
+ const kindResultOfRollout = kindResult.rollouts.find((candidate) => candidate.ruleId === rollout.ruleId);
1735
+ if (kindResultOfRollout === void 0) return [];
1736
+ const variationResults = kindResultOfRollout.variations;
1682
1737
  const table = {
1683
1738
  kind: kindResult.kind,
1684
1739
  controlVariationId: rollout.controlVariationId,
@@ -1686,7 +1741,11 @@ const kindResultLines = (role, metric, kindResult, rollout, variations) => {
1686
1741
  variations,
1687
1742
  showsChanceToBeBest: variationResults.some((variation) => variation.chanceToBeBest !== void 0)
1688
1743
  };
1689
- return [`${capitalized(roleWords(role))}: ${metric.eventName}, ${kindWords(kindResult.kind)}, ${directionWords(metric.direction)}`, ...alignedColumns(variationResults.map((variation) => resultRow(variation, table))).map((line) => ` ${line}`.trimEnd())];
1744
+ return [
1745
+ `${capitalized(roleWords(role))}: ${metric.eventName}, ${kindWords(kindResult.kind)}, ${directionWords(metric.direction)}`,
1746
+ ...alignedColumns(variationResults.map((variation) => resultRow(variation, table))).map((line) => ` ${line}`.trimEnd()),
1747
+ ...runLengthEstimateLines(kindResultOfRollout.runLengthEstimate)
1748
+ ];
1690
1749
  };
1691
1750
  const crossoverLines = (crossovers) => {
1692
1751
  if (crossovers === 0) return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Command line tool for ConfigDirector, the managed configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",