@letpeoplework/lighthouse-client 1.10.0 → 1.11.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/dist/index.cjs CHANGED
@@ -242,13 +242,26 @@ const textOf = (value) => typeof value === "string" && value.length > 0 ? value
242
242
  const isDay = (value) => isText(value) && formatCalendarDay(value) !== null;
243
243
  /** The calendar day as a person reads it, or the text exactly as sent when it is not one. */
244
244
  const dayOf = (wire) => formatCalendarDay(wire) ?? wire;
245
- /** The date a list of chances gives for one probability, or undefined when it holds none for it. */
246
- const expectedDateAt = (chances, probability) => {
245
+ /** The chance a list gives for one probability, or undefined when it holds none for it. */
246
+ const chanceAt = (chances, probability) => {
247
247
  if (!Array.isArray(chances)) return;
248
248
  const chance = chances.find((entry) => isRecord(entry) && entry.probability === probability);
249
- return isRecord(chance) ? textOf(chance.expectedDate) : void 0;
249
+ return isRecord(chance) ? chance : void 0;
250
250
  };
251
251
  //#endregion
252
+ //#region src/forecastPercentileText.ts
253
+ /**
254
+ * The years of the horizon a percentile lies beyond, or undefined when its date is the forecast. Keyed on
255
+ * the flag itself, never on the server's version: an older Lighthouse never sends it.
256
+ */
257
+ const yearsBeyondHorizon = (percentile) => isRecord(percentile) && percentile.beyondHorizon === true && isNumber(percentile.horizonYears) ? percentile.horizonYears : void 0;
258
+ /** The words shown in place of a date for a percentile too few simulated runs reached. */
259
+ const moreThanYearsText = (horizonYears) => `More than ${horizonYears} years`;
260
+ /** A percentile's day as a person reads it, or "More than 5 years" when it lies beyond that horizon. */
261
+ const describePercentileDay = (expectedDate, horizonYearsBeyond) => horizonYearsBeyond === void 0 ? dayOf(expectedDate) : moreThanYearsText(horizonYearsBeyond);
262
+ /** One percentile as text: its day, or the words when it lies beyond the horizon. */
263
+ const describePercentileDate = (percentile) => describePercentileDay(percentile.expectedDate, yearsBeyondHorizon(percentile));
264
+ //#endregion
252
265
  //#region src/ownerWording.ts
253
266
  /** A mark for a cell Lighthouse sent nothing for. */
254
267
  const NOT_SENT = "—";
@@ -408,9 +421,11 @@ const startOf = (value) => {
408
421
  observedOn: void 0,
409
422
  likelyBy: void 0
410
423
  };
424
+ const chance = chanceAt(value.percentiles, LISTED_CHANCE$1);
411
425
  return {
412
426
  observedOn: value.source === "Observed" ? textOf(value.observedDate) : void 0,
413
- likelyBy: expectedDateAt(value.percentiles, LISTED_CHANCE$1)
427
+ likelyBy: textOf(chance?.expectedDate),
428
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(chance)
414
429
  };
415
430
  };
416
431
  const readFeatureListItem = (value) => {
@@ -420,6 +435,7 @@ const readFeatureListItem = (value) => {
420
435
  const state = textOf(value.state);
421
436
  const totalWork = workOf(value.totalWork);
422
437
  const remainingWork = workOf(value.remainingWork);
438
+ const completion = chanceAt(value.forecasts, LISTED_CHANCE$1);
423
439
  if (referenceId === void 0 || name === void 0 || state === void 0 || totalWork === null || remainingWork === null) return null;
424
440
  return {
425
441
  referenceId,
@@ -429,7 +445,8 @@ const readFeatureListItem = (value) => {
429
445
  remainingWork,
430
446
  cannotBeForecast: Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0,
431
447
  start: startOf(value.startForecast),
432
- likelyBy: expectedDateAt(value.forecasts, LISTED_CHANCE$1)
448
+ likelyBy: textOf(completion?.expectedDate),
449
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(completion)
433
450
  };
434
451
  };
435
452
  /**
@@ -465,12 +482,15 @@ const describeFeatureProgress = (feature, terms) => `${feature.totalWork - featu
465
482
  const describeFeatureStart = (feature) => {
466
483
  if (feature.start.observedOn !== void 0) return dayOf(feature.start.observedOn);
467
484
  if (feature.cannotBeForecast) return CANNOT_FORECAST_SHORT;
468
- return feature.start.likelyBy === void 0 ? "—" : dayOf(feature.start.likelyBy);
485
+ return feature.start.likelyBy === void 0 ? "—" : describePercentileDay(feature.start.likelyBy, feature.start.likelyByBeyondHorizonYears);
469
486
  };
470
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
487
+ /**
488
+ * The day the Feature is 85% likely to be done by, "More than 5 years" when that lies beyond the simulation
489
+ * horizon, "Cannot forecast", or the mark when none was sent.
490
+ */
471
491
  const describeFeatureCompletion = (feature) => {
472
492
  if (feature.cannotBeForecast) return CANNOT_FORECAST_SHORT;
473
- return feature.likelyBy === void 0 ? "—" : dayOf(feature.likelyBy);
493
+ return feature.likelyBy === void 0 ? "—" : describePercentileDay(feature.likelyBy, feature.likelyByBeyondHorizonYears);
474
494
  };
475
495
  /** One Feature as a row of the list, in the order of its headings. */
476
496
  const describeFeatureRow = (feature, terms) => [
@@ -633,6 +653,7 @@ const readDeliveryListItem = (value) => {
633
653
  const name = textOf(value.name);
634
654
  const date = textOf(value.date);
635
655
  if (name === void 0 || date === void 0) return null;
656
+ const likely = chanceAt(value.completionDates, LISTED_CHANCE);
636
657
  return {
637
658
  id: value.id,
638
659
  name,
@@ -644,7 +665,8 @@ const readDeliveryListItem = (value) => {
644
665
  cannotBeForecast: teamsWithoutForecastIn(value),
645
666
  hasSufficientData: flagOf(value.hasSufficientData),
646
667
  isOverdue: flagOf(value.isOverdue),
647
- likelyBy: expectedDateAt(value.completionDates, LISTED_CHANCE)
668
+ likelyBy: textOf(likely?.expectedDate),
669
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(likely)
648
670
  };
649
671
  };
650
672
  /**
@@ -728,7 +750,7 @@ const describeDeliveryRow = (delivery, terms) => [
728
750
  delivery.featureCount === void 0 ? "—" : String(delivery.featureCount),
729
751
  describeDeliveryDone(delivery, terms),
730
752
  describeDeliveryLikelihood(delivery),
731
- delivery.likelyBy === void 0 ? "—" : dayOf(delivery.likelyBy)
753
+ delivery.likelyBy === void 0 ? "—" : describePercentileDay(delivery.likelyBy, delivery.likelyByBeyondHorizonYears)
732
754
  ];
733
755
  /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
734
756
  const describeArchivedDeliveriesTitle = (terms) => `Archived ${terms.deliveries}`;
@@ -831,18 +853,23 @@ const describeDeliveryFeatureRow = (feature) => [
831
853
  ];
832
854
  /** The forecast distribution's column headings. */
833
855
  const describeDeliveryChanceHeadings = () => ["Chance", "Done by"];
834
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
835
- const describeDeliveryChanceRow = (chance) => [`${chance.probability}%`, dayOf(chance.expectedDate)];
856
+ /** One chance of the day's forecast: "85%" and the day it is that likely to be done by, or "More than 5 years". */
857
+ const describeDeliveryChanceRow = (chance) => [`${chance.probability}%`, describePercentileDate(chance)];
836
858
  //#endregion
837
859
  //#region src/forecastWording.ts
838
- const optionalBoolean = (value) => {
860
+ const optionalBoolean$1 = (value) => {
839
861
  if (value === void 0 || typeof value === "boolean") return value;
840
862
  return null;
841
863
  };
842
- const readChanceByDay = (value) => isRecord(value) && isNumber(value.probability) && isDay(value.expectedDate) ? {
843
- probability: value.probability,
844
- expectedDate: value.expectedDate
845
- } : null;
864
+ const readChanceByDay = (value) => {
865
+ if (!isRecord(value) || !isNumber(value.probability) || !isDay(value.expectedDate)) return null;
866
+ return {
867
+ probability: value.probability,
868
+ expectedDate: value.expectedDate,
869
+ ...typeof value.beyondHorizon === "boolean" ? { beyondHorizon: value.beyondHorizon } : {},
870
+ ...isNumber(value.horizonYears) ? { horizonYears: value.horizonYears } : {}
871
+ };
872
+ };
846
873
  const readChanceOfCount = (value) => isRecord(value) && isNumber(value.probability) && isNumber(value.value) ? {
847
874
  probability: value.probability,
848
875
  value: value.value
@@ -858,8 +885,8 @@ const readManualForecast = (value) => {
858
885
  const { remainingItems, targetDate, likelihood } = value;
859
886
  const whenForecasts = readEvery$1(value.whenForecasts, readChanceByDay);
860
887
  const howManyForecasts = readEvery$1(value.howManyForecasts, readChanceOfCount);
861
- const filterApplied = optionalBoolean(value.filterApplied);
862
- const hasSufficientData = optionalBoolean(value.hasSufficientData);
888
+ const filterApplied = optionalBoolean$1(value.filterApplied);
889
+ const hasSufficientData = optionalBoolean$1(value.hasSufficientData);
863
890
  if (!isNumber(remainingItems) || !(targetDate === null || isDay(targetDate)) || !(likelihood === null || isNumber(likelihood)) || whenForecasts === null || howManyForecasts === null || filterApplied === null || hasSufficientData === null) return null;
864
891
  return {
865
892
  remainingItems,
@@ -1146,13 +1173,28 @@ const readRunChart = (value, range) => {
1146
1173
  const optionalText = (value) => isText(value) ? value : void 0;
1147
1174
  const optionalNumber = (value) => isNumber(value) ? value : void 0;
1148
1175
  const optionalDay = (value) => isDay(value) ? value : void 0;
1176
+ const optionalBoolean = (value) => typeof value === "boolean" ? value : void 0;
1177
+ const STALE_REASON_KINDS = [
1178
+ "TimeInState",
1179
+ "BlockedDuration",
1180
+ "TimeInStateWhileBlocked"
1181
+ ];
1182
+ const isStaleReasonKind = (value) => STALE_REASON_KINDS.some((kind) => kind === value);
1183
+ const readStaleReason = (value) => isRecord(value) && isStaleReasonKind(value.kind) && isNumber(value.days) ? {
1184
+ kind: value.kind,
1185
+ days: value.days,
1186
+ stateName: optionalText(value.stateName)
1187
+ } : null;
1188
+ const optionalStaleReasons = (value) => Array.isArray(value) ? value.map(readStaleReason).filter((reason) => reason !== null) : void 0;
1149
1189
  const readInProgressItem = (value) => isRecord(value) && (value.isBlocked === void 0 || typeof value.isBlocked === "boolean") ? {
1150
1190
  isBlocked: value.isBlocked,
1151
1191
  referenceId: optionalText(value.referenceId),
1152
1192
  name: optionalText(value.name),
1153
1193
  state: optionalText(value.state),
1154
1194
  workItemAge: optionalNumber(value.workItemAge),
1155
- blockedSince: optionalDay(value.blockedSince)
1195
+ blockedSince: optionalDay(value.blockedSince),
1196
+ isStale: optionalBoolean(value.isStale),
1197
+ staleReasons: optionalStaleReasons(value.staleReasons)
1156
1198
  } : null;
1157
1199
  const readInProgressNow = (value) => {
1158
1200
  if (!isRecord(value) || !isDay(value.asOfDate) || !isNumber(value.count)) return null;
@@ -1351,6 +1393,7 @@ const metricsHeadlineLabel = (part, scope, terms) => {
1351
1393
  throughput: `Total ${terms.throughput}`,
1352
1394
  arrivals: "Total Arrivals",
1353
1395
  blocked: `${terms.blocked} ${counted.many}`,
1396
+ stale: `Stale ${counted.many}`,
1354
1397
  totalWorkItemAge: `Total ${terms.workItemAge}`,
1355
1398
  predictabilityScore: "Predictability Score",
1356
1399
  cycleTimePercentiles: `${terms.cycleTime} percentiles`,
@@ -1394,27 +1437,31 @@ const describeInProgressNow = (now, scope, terms, systemWipLimit) => ({
1394
1437
  value: String(now.count),
1395
1438
  detail: systemWipLimit === void 0 ? "" : wipLimitOf(systemWipLimit, scope, terms)
1396
1439
  });
1440
+ const blockedFlag = (item) => item.isBlocked;
1441
+ const staleFlag = (item) => item.isStale;
1442
+ const saysWhichAre = (now, flag) => now.items.some((item) => flag(item) !== void 0);
1443
+ const countFlaggedNow = (part, flag, now, scope, terms) => saysWhichAre(now, flag) ? {
1444
+ label: metricsHeadlineLabel(part, scope, terms),
1445
+ value: String(now.items.filter((item) => flag(item) === true).length),
1446
+ detail: ""
1447
+ } : null;
1397
1448
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
1398
- const describeBlockedNow = (now, scope, terms) => {
1399
- if (!now.items.some((item) => item.isBlocked !== void 0)) return null;
1400
- const blocked = now.items.filter((item) => item.isBlocked === true).length;
1401
- return {
1402
- label: metricsHeadlineLabel("blocked", scope, terms),
1403
- value: String(blocked),
1404
- detail: ""
1405
- };
1406
- };
1449
+ const describeBlockedNow = (now, scope, terms) => countFlaggedNow("blocked", blockedFlag, now, scope, terms);
1450
+ /** "Stale Work Items 2", or null when Lighthouse does not say which items are stale. */
1451
+ const describeStaleNow = (now, scope, terms) => countFlaggedNow("stale", staleFlag, now, scope, terms);
1407
1452
  /**
1408
1453
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
1409
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
1410
- * A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing is said.
1454
+ * not say which Work Items are Blocked or Stale (which is not the same as none being so), and nothing in
1455
+ * progress. A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing
1456
+ * is said.
1411
1457
  */
1412
1458
  const describeWhatWipLeavesUnsaid = (now, scope, terms, systemWipLimit) => {
1413
1459
  const counted = countedOf(scope, terms);
1414
- const blockedUnknown = now.items.length > 0 && now.items.every((item) => item.isBlocked === void 0);
1460
+ const silentOn = (flag) => now.items.length > 0 && !saysWhichAre(now, flag);
1415
1461
  return [
1416
1462
  ...systemWipLimit === void 0 ? [`No System ${terms.wip} Limit is set.`] : [],
1417
- ...blockedUnknown ? [`Lighthouse does not say which ${counted.many} are ${terms.blocked}.`] : [],
1463
+ ...silentOn(blockedFlag) ? [`Lighthouse does not say which ${counted.many} are ${terms.blocked}.`] : [],
1464
+ ...silentOn(staleFlag) ? [`Lighthouse does not say which ${counted.many} are Stale.`] : [],
1418
1465
  ...now.items.length === 0 ? [`No ${counted.many} are in progress.`] : []
1419
1466
  ];
1420
1467
  };
@@ -3159,7 +3206,8 @@ const FEATURE_REQUIRES_SERVER_NEWER_THAN = {
3159
3206
  deliveryMetricsHistory: "v26.5.29.5",
3160
3207
  sleRisk: "v26.9.19.10",
3161
3208
  teamRefinement: "v26.10.3.6",
3162
- refinementVotes: "v26.10.3.6"
3209
+ refinementVotes: "v26.10.3.6",
3210
+ staleness: "v26.10.16.6"
3163
3211
  };
3164
3212
  /**
3165
3213
  * Parses a Lighthouse version string ("v26.5.24.10", "26.5.24") into numeric
@@ -3706,6 +3754,8 @@ exports.describeOwnerListHeadings = describeOwnerListHeadings;
3706
3754
  exports.describeOwnerListTitle = describeOwnerListTitle;
3707
3755
  exports.describeOwnerName = describeOwnerName;
3708
3756
  exports.describeOwnerWriteConfirmation = describeOwnerWriteConfirmation;
3757
+ exports.describePercentileDate = describePercentileDate;
3758
+ exports.describePercentileDay = describePercentileDay;
3709
3759
  exports.describePercentileRows = describePercentileRows;
3710
3760
  exports.describePercentilesOverTime = describePercentilesOverTime;
3711
3761
  exports.describePercentilesOverTimeDays = describePercentilesOverTimeDays;
@@ -3726,6 +3776,7 @@ exports.describeRefinementSummary = describeRefinementSummary;
3726
3776
  exports.describeRefreshConfirmation = describeRefreshConfirmation;
3727
3777
  exports.describeRefusedMetric = describeRefusedMetric;
3728
3778
  exports.describeSleRiskNow = describeSleRiskNow;
3779
+ exports.describeStaleNow = describeStaleNow;
3729
3780
  exports.describeTags = describeTags;
3730
3781
  exports.describeTakenBack = describeTakenBack;
3731
3782
  exports.describeTeamSummary = describeTeamSummary;
@@ -3774,6 +3825,7 @@ exports.likelihoodAnswer = likelihoodAnswer;
3774
3825
  exports.loadStandaloneDiscoveryContract = loadStandaloneDiscoveryContract;
3775
3826
  exports.metricsHeadlineLabel = metricsHeadlineLabel;
3776
3827
  exports.mintVoterKey = mintVoterKey;
3828
+ exports.moreThanYearsText = moreThanYearsText;
3777
3829
  exports.myVoteOn = myVoteOn;
3778
3830
  exports.nameOrFallback = nameOrFallback;
3779
3831
  exports.ordinalOf = ordinalOf;
@@ -3845,3 +3897,4 @@ exports.usageDataStoreFor = usageDataStoreFor;
3845
3897
  exports.validateLighthouseConnectivity = validateLighthouseConnectivity;
3846
3898
  exports.voterKeyStoreFor = voterKeyStoreFor;
3847
3899
  exports.withdrawUsageData = withdrawUsageData;
3900
+ exports.yearsBeyondHorizon = yearsBeyondHorizon;
package/dist/index.d.cts CHANGED
@@ -195,6 +195,7 @@ export type DeliveryListItem = {
195
195
  readonly hasSufficientData: boolean | undefined;
196
196
  readonly isOverdue: boolean | undefined;
197
197
  readonly likelyBy: string | undefined;
198
+ readonly likelyByBeyondHorizonYears?: number;
198
199
  };
199
200
  /** The Portfolio whose Deliveries are listed; its name stays undefined when it could not be read. */
200
201
  export type DeliveryListOwner = {
@@ -269,7 +270,7 @@ export declare const describeDeliveryFeatureHeadings: (terms: Terms) => string[]
269
270
  export declare const describeDeliveryFeatureRow: (feature: DeliveryFeatureMetric) => string[];
270
271
  /** The forecast distribution's column headings. */
271
272
  export declare const describeDeliveryChanceHeadings: () => string[];
272
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
273
+ /** One chance of the day's forecast: "85%" and the day it is that likely to be done by, or "More than 5 years". */
273
274
  export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributionPoint) => string[];
274
275
  //#endregion
275
276
  //#region src/featureWording.d.ts
@@ -277,6 +278,7 @@ export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributio
277
278
  export type FeatureStart = {
278
279
  readonly observedOn: string | undefined;
279
280
  readonly likelyBy: string | undefined;
281
+ readonly likelyByBeyondHorizonYears?: number;
280
282
  };
281
283
  /** One Feature as a row of the Feature list states it. */
282
284
  export type FeatureListItem = {
@@ -288,6 +290,7 @@ export type FeatureListItem = {
288
290
  readonly cannotBeForecast: boolean;
289
291
  readonly start: FeatureStart;
290
292
  readonly likelyBy: string | undefined;
293
+ readonly likelyByBeyondHorizonYears?: number;
291
294
  };
292
295
  /**
293
296
  * Every Feature in the answer, or null when it is not a list or any one of them comes without its
@@ -305,8 +308,11 @@ export declare const describeFeatureProgress: (feature: Pick<FeatureListItem, "t
305
308
  * Team without history; that Team only stops anyone saying when work will begin.
306
309
  */
307
310
  export declare const describeFeatureStart: (feature: Pick<FeatureListItem, "start" | "cannotBeForecast">) => string;
308
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
309
- export declare const describeFeatureCompletion: (feature: Pick<FeatureListItem, "likelyBy" | "cannotBeForecast">) => string;
311
+ /**
312
+ * The day the Feature is 85% likely to be done by, "More than 5 years" when that lies beyond the simulation
313
+ * horizon, "Cannot forecast", or the mark when none was sent.
314
+ */
315
+ export declare const describeFeatureCompletion: (feature: Pick<FeatureListItem, "likelyBy" | "likelyByBeyondHorizonYears" | "cannotBeForecast">) => string;
310
316
  /** One Feature as a row of the list, in the order of its headings. */
311
317
  export declare const describeFeatureRow: (feature: FeatureListItem, terms: Terms) => string[];
312
318
  /** One Work Item of a Feature as the Feature's Work Items list states it. */
@@ -357,11 +363,33 @@ export declare const deliveryLikelihoodAnswer: (facts: Parameters<typeof likelih
357
363
  readonly isOverdue?: boolean;
358
364
  }) => string;
359
365
  //#endregion
366
+ //#region src/forecastPercentileText.d.ts
367
+ /**
368
+ * A forecast percentile as Lighthouse sends it. A percentile too few simulated runs reached carries
369
+ * `beyondHorizon` and the horizon's length in `horizonYears`; its `expectedDate` is then only a stand-in
370
+ * that nothing forecast, kept so an older client still reads a date. An older Lighthouse sends neither.
371
+ */
372
+ export type ForecastPercentileDate = {
373
+ readonly expectedDate: string;
374
+ readonly beyondHorizon?: boolean;
375
+ readonly horizonYears?: number | null;
376
+ };
377
+ /**
378
+ * The years of the horizon a percentile lies beyond, or undefined when its date is the forecast. Keyed on
379
+ * the flag itself, never on the server's version: an older Lighthouse never sends it.
380
+ */
381
+ export declare const yearsBeyondHorizon: (percentile: unknown) => number | undefined;
382
+ /** The words shown in place of a date for a percentile too few simulated runs reached. */
383
+ export declare const moreThanYearsText: (horizonYears: number) => string;
384
+ /** A percentile's day as a person reads it, or "More than 5 years" when it lies beyond that horizon. */
385
+ export declare const describePercentileDay: (expectedDate: string, horizonYearsBeyond: number | undefined) => string;
386
+ /** One percentile as text: its day, or the words when it lies beyond the horizon. */
387
+ export declare const describePercentileDate: (percentile: ForecastPercentileDate) => string;
388
+ //#endregion
360
389
  //#region src/forecastWording.d.ts
361
390
  /** One row of the When table: the chance of being done by a calendar day. */
362
- export type ForecastChanceByDay = {
391
+ export type ForecastChanceByDay = ForecastPercentileDate & {
363
392
  readonly probability: number;
364
- readonly expectedDate: string;
365
393
  };
366
394
  /** One row of the How Many table: the chance of getting this many done. */
367
395
  export type ForecastChanceOfCount = {
@@ -532,7 +560,15 @@ export declare const readArrivals: typeof readDailyCountChart;
532
560
  * range's first day, read as the total and the count of each day. Without a total the days are summed.
533
561
  */
534
562
  export declare const readRunChart: (value: unknown, range: MetricsDateRange) => DailyCountChartView | null;
535
- /** A Work Item in progress; an older Lighthouse does not say whether it is blocked. */
563
+ declare const STALE_REASON_KINDS: readonly ["TimeInState", "BlockedDuration", "TimeInStateWhileBlocked"];
564
+ type StaleReasonKind = (typeof STALE_REASON_KINDS)[number];
565
+ /** Why Lighthouse marks a Work Item stale, in whole days on the instance's calendar. */
566
+ export type StaleReason = {
567
+ readonly kind: StaleReasonKind;
568
+ readonly days: number;
569
+ readonly stateName?: string;
570
+ };
571
+ /** A Work Item in progress; an older Lighthouse does not say whether it is blocked, or stale. */
536
572
  export type InProgressItem = {
537
573
  readonly isBlocked: boolean | undefined;
538
574
  readonly referenceId?: string;
@@ -540,6 +576,8 @@ export type InProgressItem = {
540
576
  readonly state?: string;
541
577
  readonly workItemAge?: number;
542
578
  readonly blockedSince?: string;
579
+ readonly isStale?: boolean;
580
+ readonly staleReasons?: readonly StaleReason[];
543
581
  };
544
582
  export type InProgressNowView = {
545
583
  readonly asOfDate: string;
@@ -621,7 +659,7 @@ export declare const PREDICTABILITY_SCORE_EXPLANATION = "The predictability scor
621
659
  /** What an empty over-time chart says on the web. */
622
660
  export declare const OVER_TIME_EMPTY_SENTENCE = "Nothing to show for the selected range. Days appear here as Lighthouse records them.";
623
661
  /** Every part of the headline that can be refused or unreadable on its own. */
624
- export type MetricsHeadlinePart = "wip" | "throughput" | "arrivals" | "blocked" | "totalWorkItemAge" | "predictabilityScore" | "cycleTimePercentiles" | "workItemAgePercentiles" | "workItemAge" | "cumulativeStateTime" | "percentilesOverTime" | "processBehaviorOverTime";
662
+ export type MetricsHeadlinePart = "wip" | "throughput" | "arrivals" | "blocked" | "stale" | "totalWorkItemAge" | "predictabilityScore" | "cycleTimePercentiles" | "workItemAgePercentiles" | "workItemAge" | "cumulativeStateTime" | "percentilesOverTime" | "processBehaviorOverTime";
625
663
  /** "1 day", "12 days". */
626
664
  export declare const describeDays: (days: number) => string;
627
665
  /** The label each part of the headline goes by, in the instance's words. */
@@ -640,10 +678,13 @@ export declare const describeAsOfHeading: (subject: Pick<MetricsSubject, "endDat
640
678
  export declare const describeInProgressNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined) => MetricLine;
641
679
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
642
680
  export declare const describeBlockedNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms) => MetricLine | null;
681
+ /** "Stale Work Items 2", or null when Lighthouse does not say which items are stale. */
682
+ export declare const describeStaleNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms) => MetricLine | null;
643
683
  /**
644
684
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
645
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
646
- * A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing is said.
685
+ * not say which Work Items are Blocked or Stale (which is not the same as none being so), and nothing in
686
+ * progress. A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing
687
+ * is said.
647
688
  */
648
689
  export declare const describeWhatWipLeavesUnsaid: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined | null) => readonly string[];
649
690
  /** "Total Throughput 31 1.0 / day". */
@@ -1431,6 +1472,10 @@ export type DeliveryFeatureMetric = {
1431
1472
  export type DeliveryWhenDistributionPoint = {
1432
1473
  readonly probability: number;
1433
1474
  readonly expectedDate: string;
1475
+ /** True when too few simulated runs reached this chance: `expectedDate` is then only a stand-in. Absent on an older Lighthouse. */
1476
+ readonly beyondHorizon?: boolean;
1477
+ /** The simulation horizon, in years, this chance was forecast with. Absent on an older Lighthouse and on days recorded before it. */
1478
+ readonly horizonYears?: number | null;
1434
1479
  };
1435
1480
  /** A delivery as it stood on one recorded day. Dates are ISO strings. */
1436
1481
  export type DeliveryMetricsHistoryPoint = {
@@ -1851,6 +1896,7 @@ export declare const FEATURE_REQUIRES_SERVER_NEWER_THAN: {
1851
1896
  readonly sleRisk: "v26.9.19.10";
1852
1897
  readonly teamRefinement: "v26.10.3.6";
1853
1898
  readonly refinementVotes: "v26.10.3.6";
1899
+ readonly staleness: "v26.10.16.6";
1854
1900
  };
1855
1901
  /**
1856
1902
  * Returns true when `candidate` is strictly newer than `baseline`, false when
@@ -1861,4 +1907,5 @@ export declare const isServerVersionNewerThan: (candidate: string, baseline: str
1861
1907
  /** The header a client sends its voter key in, so Lighthouse can tell its votes from anyone else's. */
1862
1908
  export declare const VOTER_KEY_HEADER = "X-Lighthouse-Voter-Key";
1863
1909
  export declare const createLighthouseClient: (configuration: LighthouseClientConfiguration, dependencies?: LighthouseClientDependencies) => LighthouseClient;
1864
- //#endregion
1910
+ //#endregion
1911
+ export {};
package/dist/index.d.ts CHANGED
@@ -195,6 +195,7 @@ export type DeliveryListItem = {
195
195
  readonly hasSufficientData: boolean | undefined;
196
196
  readonly isOverdue: boolean | undefined;
197
197
  readonly likelyBy: string | undefined;
198
+ readonly likelyByBeyondHorizonYears?: number;
198
199
  };
199
200
  /** The Portfolio whose Deliveries are listed; its name stays undefined when it could not be read. */
200
201
  export type DeliveryListOwner = {
@@ -269,7 +270,7 @@ export declare const describeDeliveryFeatureHeadings: (terms: Terms) => string[]
269
270
  export declare const describeDeliveryFeatureRow: (feature: DeliveryFeatureMetric) => string[];
270
271
  /** The forecast distribution's column headings. */
271
272
  export declare const describeDeliveryChanceHeadings: () => string[];
272
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
273
+ /** One chance of the day's forecast: "85%" and the day it is that likely to be done by, or "More than 5 years". */
273
274
  export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributionPoint) => string[];
274
275
  //#endregion
275
276
  //#region src/featureWording.d.ts
@@ -277,6 +278,7 @@ export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributio
277
278
  export type FeatureStart = {
278
279
  readonly observedOn: string | undefined;
279
280
  readonly likelyBy: string | undefined;
281
+ readonly likelyByBeyondHorizonYears?: number;
280
282
  };
281
283
  /** One Feature as a row of the Feature list states it. */
282
284
  export type FeatureListItem = {
@@ -288,6 +290,7 @@ export type FeatureListItem = {
288
290
  readonly cannotBeForecast: boolean;
289
291
  readonly start: FeatureStart;
290
292
  readonly likelyBy: string | undefined;
293
+ readonly likelyByBeyondHorizonYears?: number;
291
294
  };
292
295
  /**
293
296
  * Every Feature in the answer, or null when it is not a list or any one of them comes without its
@@ -305,8 +308,11 @@ export declare const describeFeatureProgress: (feature: Pick<FeatureListItem, "t
305
308
  * Team without history; that Team only stops anyone saying when work will begin.
306
309
  */
307
310
  export declare const describeFeatureStart: (feature: Pick<FeatureListItem, "start" | "cannotBeForecast">) => string;
308
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
309
- export declare const describeFeatureCompletion: (feature: Pick<FeatureListItem, "likelyBy" | "cannotBeForecast">) => string;
311
+ /**
312
+ * The day the Feature is 85% likely to be done by, "More than 5 years" when that lies beyond the simulation
313
+ * horizon, "Cannot forecast", or the mark when none was sent.
314
+ */
315
+ export declare const describeFeatureCompletion: (feature: Pick<FeatureListItem, "likelyBy" | "likelyByBeyondHorizonYears" | "cannotBeForecast">) => string;
310
316
  /** One Feature as a row of the list, in the order of its headings. */
311
317
  export declare const describeFeatureRow: (feature: FeatureListItem, terms: Terms) => string[];
312
318
  /** One Work Item of a Feature as the Feature's Work Items list states it. */
@@ -357,11 +363,33 @@ export declare const deliveryLikelihoodAnswer: (facts: Parameters<typeof likelih
357
363
  readonly isOverdue?: boolean;
358
364
  }) => string;
359
365
  //#endregion
366
+ //#region src/forecastPercentileText.d.ts
367
+ /**
368
+ * A forecast percentile as Lighthouse sends it. A percentile too few simulated runs reached carries
369
+ * `beyondHorizon` and the horizon's length in `horizonYears`; its `expectedDate` is then only a stand-in
370
+ * that nothing forecast, kept so an older client still reads a date. An older Lighthouse sends neither.
371
+ */
372
+ export type ForecastPercentileDate = {
373
+ readonly expectedDate: string;
374
+ readonly beyondHorizon?: boolean;
375
+ readonly horizonYears?: number | null;
376
+ };
377
+ /**
378
+ * The years of the horizon a percentile lies beyond, or undefined when its date is the forecast. Keyed on
379
+ * the flag itself, never on the server's version: an older Lighthouse never sends it.
380
+ */
381
+ export declare const yearsBeyondHorizon: (percentile: unknown) => number | undefined;
382
+ /** The words shown in place of a date for a percentile too few simulated runs reached. */
383
+ export declare const moreThanYearsText: (horizonYears: number) => string;
384
+ /** A percentile's day as a person reads it, or "More than 5 years" when it lies beyond that horizon. */
385
+ export declare const describePercentileDay: (expectedDate: string, horizonYearsBeyond: number | undefined) => string;
386
+ /** One percentile as text: its day, or the words when it lies beyond the horizon. */
387
+ export declare const describePercentileDate: (percentile: ForecastPercentileDate) => string;
388
+ //#endregion
360
389
  //#region src/forecastWording.d.ts
361
390
  /** One row of the When table: the chance of being done by a calendar day. */
362
- export type ForecastChanceByDay = {
391
+ export type ForecastChanceByDay = ForecastPercentileDate & {
363
392
  readonly probability: number;
364
- readonly expectedDate: string;
365
393
  };
366
394
  /** One row of the How Many table: the chance of getting this many done. */
367
395
  export type ForecastChanceOfCount = {
@@ -532,7 +560,15 @@ export declare const readArrivals: typeof readDailyCountChart;
532
560
  * range's first day, read as the total and the count of each day. Without a total the days are summed.
533
561
  */
534
562
  export declare const readRunChart: (value: unknown, range: MetricsDateRange) => DailyCountChartView | null;
535
- /** A Work Item in progress; an older Lighthouse does not say whether it is blocked. */
563
+ declare const STALE_REASON_KINDS: readonly ["TimeInState", "BlockedDuration", "TimeInStateWhileBlocked"];
564
+ type StaleReasonKind = (typeof STALE_REASON_KINDS)[number];
565
+ /** Why Lighthouse marks a Work Item stale, in whole days on the instance's calendar. */
566
+ export type StaleReason = {
567
+ readonly kind: StaleReasonKind;
568
+ readonly days: number;
569
+ readonly stateName?: string;
570
+ };
571
+ /** A Work Item in progress; an older Lighthouse does not say whether it is blocked, or stale. */
536
572
  export type InProgressItem = {
537
573
  readonly isBlocked: boolean | undefined;
538
574
  readonly referenceId?: string;
@@ -540,6 +576,8 @@ export type InProgressItem = {
540
576
  readonly state?: string;
541
577
  readonly workItemAge?: number;
542
578
  readonly blockedSince?: string;
579
+ readonly isStale?: boolean;
580
+ readonly staleReasons?: readonly StaleReason[];
543
581
  };
544
582
  export type InProgressNowView = {
545
583
  readonly asOfDate: string;
@@ -621,7 +659,7 @@ export declare const PREDICTABILITY_SCORE_EXPLANATION = "The predictability scor
621
659
  /** What an empty over-time chart says on the web. */
622
660
  export declare const OVER_TIME_EMPTY_SENTENCE = "Nothing to show for the selected range. Days appear here as Lighthouse records them.";
623
661
  /** Every part of the headline that can be refused or unreadable on its own. */
624
- export type MetricsHeadlinePart = "wip" | "throughput" | "arrivals" | "blocked" | "totalWorkItemAge" | "predictabilityScore" | "cycleTimePercentiles" | "workItemAgePercentiles" | "workItemAge" | "cumulativeStateTime" | "percentilesOverTime" | "processBehaviorOverTime";
662
+ export type MetricsHeadlinePart = "wip" | "throughput" | "arrivals" | "blocked" | "stale" | "totalWorkItemAge" | "predictabilityScore" | "cycleTimePercentiles" | "workItemAgePercentiles" | "workItemAge" | "cumulativeStateTime" | "percentilesOverTime" | "processBehaviorOverTime";
625
663
  /** "1 day", "12 days". */
626
664
  export declare const describeDays: (days: number) => string;
627
665
  /** The label each part of the headline goes by, in the instance's words. */
@@ -640,10 +678,13 @@ export declare const describeAsOfHeading: (subject: Pick<MetricsSubject, "endDat
640
678
  export declare const describeInProgressNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined) => MetricLine;
641
679
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
642
680
  export declare const describeBlockedNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms) => MetricLine | null;
681
+ /** "Stale Work Items 2", or null when Lighthouse does not say which items are stale. */
682
+ export declare const describeStaleNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms) => MetricLine | null;
643
683
  /**
644
684
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
645
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
646
- * A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing is said.
685
+ * not say which Work Items are Blocked or Stale (which is not the same as none being so), and nothing in
686
+ * progress. A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing
687
+ * is said.
647
688
  */
648
689
  export declare const describeWhatWipLeavesUnsaid: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined | null) => readonly string[];
649
690
  /** "Total Throughput 31 1.0 / day". */
@@ -1431,6 +1472,10 @@ export type DeliveryFeatureMetric = {
1431
1472
  export type DeliveryWhenDistributionPoint = {
1432
1473
  readonly probability: number;
1433
1474
  readonly expectedDate: string;
1475
+ /** True when too few simulated runs reached this chance: `expectedDate` is then only a stand-in. Absent on an older Lighthouse. */
1476
+ readonly beyondHorizon?: boolean;
1477
+ /** The simulation horizon, in years, this chance was forecast with. Absent on an older Lighthouse and on days recorded before it. */
1478
+ readonly horizonYears?: number | null;
1434
1479
  };
1435
1480
  /** A delivery as it stood on one recorded day. Dates are ISO strings. */
1436
1481
  export type DeliveryMetricsHistoryPoint = {
@@ -1851,6 +1896,7 @@ export declare const FEATURE_REQUIRES_SERVER_NEWER_THAN: {
1851
1896
  readonly sleRisk: "v26.9.19.10";
1852
1897
  readonly teamRefinement: "v26.10.3.6";
1853
1898
  readonly refinementVotes: "v26.10.3.6";
1899
+ readonly staleness: "v26.10.16.6";
1854
1900
  };
1855
1901
  /**
1856
1902
  * Returns true when `candidate` is strictly newer than `baseline`, false when
@@ -1861,4 +1907,5 @@ export declare const isServerVersionNewerThan: (candidate: string, baseline: str
1861
1907
  /** The header a client sends its voter key in, so Lighthouse can tell its votes from anyone else's. */
1862
1908
  export declare const VOTER_KEY_HEADER = "X-Lighthouse-Voter-Key";
1863
1909
  export declare const createLighthouseClient: (configuration: LighthouseClientConfiguration, dependencies?: LighthouseClientDependencies) => LighthouseClient;
1864
- //#endregion
1910
+ //#endregion
1911
+ export {};
package/dist/index.js CHANGED
@@ -241,13 +241,26 @@ const textOf = (value) => typeof value === "string" && value.length > 0 ? value
241
241
  const isDay = (value) => isText(value) && formatCalendarDay(value) !== null;
242
242
  /** The calendar day as a person reads it, or the text exactly as sent when it is not one. */
243
243
  const dayOf = (wire) => formatCalendarDay(wire) ?? wire;
244
- /** The date a list of chances gives for one probability, or undefined when it holds none for it. */
245
- const expectedDateAt = (chances, probability) => {
244
+ /** The chance a list gives for one probability, or undefined when it holds none for it. */
245
+ const chanceAt = (chances, probability) => {
246
246
  if (!Array.isArray(chances)) return;
247
247
  const chance = chances.find((entry) => isRecord(entry) && entry.probability === probability);
248
- return isRecord(chance) ? textOf(chance.expectedDate) : void 0;
248
+ return isRecord(chance) ? chance : void 0;
249
249
  };
250
250
  //#endregion
251
+ //#region src/forecastPercentileText.ts
252
+ /**
253
+ * The years of the horizon a percentile lies beyond, or undefined when its date is the forecast. Keyed on
254
+ * the flag itself, never on the server's version: an older Lighthouse never sends it.
255
+ */
256
+ const yearsBeyondHorizon = (percentile) => isRecord(percentile) && percentile.beyondHorizon === true && isNumber(percentile.horizonYears) ? percentile.horizonYears : void 0;
257
+ /** The words shown in place of a date for a percentile too few simulated runs reached. */
258
+ const moreThanYearsText = (horizonYears) => `More than ${horizonYears} years`;
259
+ /** A percentile's day as a person reads it, or "More than 5 years" when it lies beyond that horizon. */
260
+ const describePercentileDay = (expectedDate, horizonYearsBeyond) => horizonYearsBeyond === void 0 ? dayOf(expectedDate) : moreThanYearsText(horizonYearsBeyond);
261
+ /** One percentile as text: its day, or the words when it lies beyond the horizon. */
262
+ const describePercentileDate = (percentile) => describePercentileDay(percentile.expectedDate, yearsBeyondHorizon(percentile));
263
+ //#endregion
251
264
  //#region src/ownerWording.ts
252
265
  /** A mark for a cell Lighthouse sent nothing for. */
253
266
  const NOT_SENT = "—";
@@ -407,9 +420,11 @@ const startOf = (value) => {
407
420
  observedOn: void 0,
408
421
  likelyBy: void 0
409
422
  };
423
+ const chance = chanceAt(value.percentiles, LISTED_CHANCE$1);
410
424
  return {
411
425
  observedOn: value.source === "Observed" ? textOf(value.observedDate) : void 0,
412
- likelyBy: expectedDateAt(value.percentiles, LISTED_CHANCE$1)
426
+ likelyBy: textOf(chance?.expectedDate),
427
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(chance)
413
428
  };
414
429
  };
415
430
  const readFeatureListItem = (value) => {
@@ -419,6 +434,7 @@ const readFeatureListItem = (value) => {
419
434
  const state = textOf(value.state);
420
435
  const totalWork = workOf(value.totalWork);
421
436
  const remainingWork = workOf(value.remainingWork);
437
+ const completion = chanceAt(value.forecasts, LISTED_CHANCE$1);
422
438
  if (referenceId === void 0 || name === void 0 || state === void 0 || totalWork === null || remainingWork === null) return null;
423
439
  return {
424
440
  referenceId,
@@ -428,7 +444,8 @@ const readFeatureListItem = (value) => {
428
444
  remainingWork,
429
445
  cannotBeForecast: Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0,
430
446
  start: startOf(value.startForecast),
431
- likelyBy: expectedDateAt(value.forecasts, LISTED_CHANCE$1)
447
+ likelyBy: textOf(completion?.expectedDate),
448
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(completion)
432
449
  };
433
450
  };
434
451
  /**
@@ -464,12 +481,15 @@ const describeFeatureProgress = (feature, terms) => `${feature.totalWork - featu
464
481
  const describeFeatureStart = (feature) => {
465
482
  if (feature.start.observedOn !== void 0) return dayOf(feature.start.observedOn);
466
483
  if (feature.cannotBeForecast) return CANNOT_FORECAST_SHORT;
467
- return feature.start.likelyBy === void 0 ? "—" : dayOf(feature.start.likelyBy);
484
+ return feature.start.likelyBy === void 0 ? "—" : describePercentileDay(feature.start.likelyBy, feature.start.likelyByBeyondHorizonYears);
468
485
  };
469
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
486
+ /**
487
+ * The day the Feature is 85% likely to be done by, "More than 5 years" when that lies beyond the simulation
488
+ * horizon, "Cannot forecast", or the mark when none was sent.
489
+ */
470
490
  const describeFeatureCompletion = (feature) => {
471
491
  if (feature.cannotBeForecast) return CANNOT_FORECAST_SHORT;
472
- return feature.likelyBy === void 0 ? "—" : dayOf(feature.likelyBy);
492
+ return feature.likelyBy === void 0 ? "—" : describePercentileDay(feature.likelyBy, feature.likelyByBeyondHorizonYears);
473
493
  };
474
494
  /** One Feature as a row of the list, in the order of its headings. */
475
495
  const describeFeatureRow = (feature, terms) => [
@@ -632,6 +652,7 @@ const readDeliveryListItem = (value) => {
632
652
  const name = textOf(value.name);
633
653
  const date = textOf(value.date);
634
654
  if (name === void 0 || date === void 0) return null;
655
+ const likely = chanceAt(value.completionDates, LISTED_CHANCE);
635
656
  return {
636
657
  id: value.id,
637
658
  name,
@@ -643,7 +664,8 @@ const readDeliveryListItem = (value) => {
643
664
  cannotBeForecast: teamsWithoutForecastIn(value),
644
665
  hasSufficientData: flagOf(value.hasSufficientData),
645
666
  isOverdue: flagOf(value.isOverdue),
646
- likelyBy: expectedDateAt(value.completionDates, LISTED_CHANCE)
667
+ likelyBy: textOf(likely?.expectedDate),
668
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(likely)
647
669
  };
648
670
  };
649
671
  /**
@@ -727,7 +749,7 @@ const describeDeliveryRow = (delivery, terms) => [
727
749
  delivery.featureCount === void 0 ? "—" : String(delivery.featureCount),
728
750
  describeDeliveryDone(delivery, terms),
729
751
  describeDeliveryLikelihood(delivery),
730
- delivery.likelyBy === void 0 ? "—" : dayOf(delivery.likelyBy)
752
+ delivery.likelyBy === void 0 ? "—" : describePercentileDay(delivery.likelyBy, delivery.likelyByBeyondHorizonYears)
731
753
  ];
732
754
  /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
733
755
  const describeArchivedDeliveriesTitle = (terms) => `Archived ${terms.deliveries}`;
@@ -830,18 +852,23 @@ const describeDeliveryFeatureRow = (feature) => [
830
852
  ];
831
853
  /** The forecast distribution's column headings. */
832
854
  const describeDeliveryChanceHeadings = () => ["Chance", "Done by"];
833
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
834
- const describeDeliveryChanceRow = (chance) => [`${chance.probability}%`, dayOf(chance.expectedDate)];
855
+ /** One chance of the day's forecast: "85%" and the day it is that likely to be done by, or "More than 5 years". */
856
+ const describeDeliveryChanceRow = (chance) => [`${chance.probability}%`, describePercentileDate(chance)];
835
857
  //#endregion
836
858
  //#region src/forecastWording.ts
837
- const optionalBoolean = (value) => {
859
+ const optionalBoolean$1 = (value) => {
838
860
  if (value === void 0 || typeof value === "boolean") return value;
839
861
  return null;
840
862
  };
841
- const readChanceByDay = (value) => isRecord(value) && isNumber(value.probability) && isDay(value.expectedDate) ? {
842
- probability: value.probability,
843
- expectedDate: value.expectedDate
844
- } : null;
863
+ const readChanceByDay = (value) => {
864
+ if (!isRecord(value) || !isNumber(value.probability) || !isDay(value.expectedDate)) return null;
865
+ return {
866
+ probability: value.probability,
867
+ expectedDate: value.expectedDate,
868
+ ...typeof value.beyondHorizon === "boolean" ? { beyondHorizon: value.beyondHorizon } : {},
869
+ ...isNumber(value.horizonYears) ? { horizonYears: value.horizonYears } : {}
870
+ };
871
+ };
845
872
  const readChanceOfCount = (value) => isRecord(value) && isNumber(value.probability) && isNumber(value.value) ? {
846
873
  probability: value.probability,
847
874
  value: value.value
@@ -857,8 +884,8 @@ const readManualForecast = (value) => {
857
884
  const { remainingItems, targetDate, likelihood } = value;
858
885
  const whenForecasts = readEvery$1(value.whenForecasts, readChanceByDay);
859
886
  const howManyForecasts = readEvery$1(value.howManyForecasts, readChanceOfCount);
860
- const filterApplied = optionalBoolean(value.filterApplied);
861
- const hasSufficientData = optionalBoolean(value.hasSufficientData);
887
+ const filterApplied = optionalBoolean$1(value.filterApplied);
888
+ const hasSufficientData = optionalBoolean$1(value.hasSufficientData);
862
889
  if (!isNumber(remainingItems) || !(targetDate === null || isDay(targetDate)) || !(likelihood === null || isNumber(likelihood)) || whenForecasts === null || howManyForecasts === null || filterApplied === null || hasSufficientData === null) return null;
863
890
  return {
864
891
  remainingItems,
@@ -1145,13 +1172,28 @@ const readRunChart = (value, range) => {
1145
1172
  const optionalText = (value) => isText(value) ? value : void 0;
1146
1173
  const optionalNumber = (value) => isNumber(value) ? value : void 0;
1147
1174
  const optionalDay = (value) => isDay(value) ? value : void 0;
1175
+ const optionalBoolean = (value) => typeof value === "boolean" ? value : void 0;
1176
+ const STALE_REASON_KINDS = [
1177
+ "TimeInState",
1178
+ "BlockedDuration",
1179
+ "TimeInStateWhileBlocked"
1180
+ ];
1181
+ const isStaleReasonKind = (value) => STALE_REASON_KINDS.some((kind) => kind === value);
1182
+ const readStaleReason = (value) => isRecord(value) && isStaleReasonKind(value.kind) && isNumber(value.days) ? {
1183
+ kind: value.kind,
1184
+ days: value.days,
1185
+ stateName: optionalText(value.stateName)
1186
+ } : null;
1187
+ const optionalStaleReasons = (value) => Array.isArray(value) ? value.map(readStaleReason).filter((reason) => reason !== null) : void 0;
1148
1188
  const readInProgressItem = (value) => isRecord(value) && (value.isBlocked === void 0 || typeof value.isBlocked === "boolean") ? {
1149
1189
  isBlocked: value.isBlocked,
1150
1190
  referenceId: optionalText(value.referenceId),
1151
1191
  name: optionalText(value.name),
1152
1192
  state: optionalText(value.state),
1153
1193
  workItemAge: optionalNumber(value.workItemAge),
1154
- blockedSince: optionalDay(value.blockedSince)
1194
+ blockedSince: optionalDay(value.blockedSince),
1195
+ isStale: optionalBoolean(value.isStale),
1196
+ staleReasons: optionalStaleReasons(value.staleReasons)
1155
1197
  } : null;
1156
1198
  const readInProgressNow = (value) => {
1157
1199
  if (!isRecord(value) || !isDay(value.asOfDate) || !isNumber(value.count)) return null;
@@ -1350,6 +1392,7 @@ const metricsHeadlineLabel = (part, scope, terms) => {
1350
1392
  throughput: `Total ${terms.throughput}`,
1351
1393
  arrivals: "Total Arrivals",
1352
1394
  blocked: `${terms.blocked} ${counted.many}`,
1395
+ stale: `Stale ${counted.many}`,
1353
1396
  totalWorkItemAge: `Total ${terms.workItemAge}`,
1354
1397
  predictabilityScore: "Predictability Score",
1355
1398
  cycleTimePercentiles: `${terms.cycleTime} percentiles`,
@@ -1393,27 +1436,31 @@ const describeInProgressNow = (now, scope, terms, systemWipLimit) => ({
1393
1436
  value: String(now.count),
1394
1437
  detail: systemWipLimit === void 0 ? "" : wipLimitOf(systemWipLimit, scope, terms)
1395
1438
  });
1439
+ const blockedFlag = (item) => item.isBlocked;
1440
+ const staleFlag = (item) => item.isStale;
1441
+ const saysWhichAre = (now, flag) => now.items.some((item) => flag(item) !== void 0);
1442
+ const countFlaggedNow = (part, flag, now, scope, terms) => saysWhichAre(now, flag) ? {
1443
+ label: metricsHeadlineLabel(part, scope, terms),
1444
+ value: String(now.items.filter((item) => flag(item) === true).length),
1445
+ detail: ""
1446
+ } : null;
1396
1447
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
1397
- const describeBlockedNow = (now, scope, terms) => {
1398
- if (!now.items.some((item) => item.isBlocked !== void 0)) return null;
1399
- const blocked = now.items.filter((item) => item.isBlocked === true).length;
1400
- return {
1401
- label: metricsHeadlineLabel("blocked", scope, terms),
1402
- value: String(blocked),
1403
- detail: ""
1404
- };
1405
- };
1448
+ const describeBlockedNow = (now, scope, terms) => countFlaggedNow("blocked", blockedFlag, now, scope, terms);
1449
+ /** "Stale Work Items 2", or null when Lighthouse does not say which items are stale. */
1450
+ const describeStaleNow = (now, scope, terms) => countFlaggedNow("stale", staleFlag, now, scope, terms);
1406
1451
  /**
1407
1452
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
1408
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
1409
- * A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing is said.
1453
+ * not say which Work Items are Blocked or Stale (which is not the same as none being so), and nothing in
1454
+ * progress. A limit of null means the Team or Portfolio could not be read; it may well have one, so nothing
1455
+ * is said.
1410
1456
  */
1411
1457
  const describeWhatWipLeavesUnsaid = (now, scope, terms, systemWipLimit) => {
1412
1458
  const counted = countedOf(scope, terms);
1413
- const blockedUnknown = now.items.length > 0 && now.items.every((item) => item.isBlocked === void 0);
1459
+ const silentOn = (flag) => now.items.length > 0 && !saysWhichAre(now, flag);
1414
1460
  return [
1415
1461
  ...systemWipLimit === void 0 ? [`No System ${terms.wip} Limit is set.`] : [],
1416
- ...blockedUnknown ? [`Lighthouse does not say which ${counted.many} are ${terms.blocked}.`] : [],
1462
+ ...silentOn(blockedFlag) ? [`Lighthouse does not say which ${counted.many} are ${terms.blocked}.`] : [],
1463
+ ...silentOn(staleFlag) ? [`Lighthouse does not say which ${counted.many} are Stale.`] : [],
1417
1464
  ...now.items.length === 0 ? [`No ${counted.many} are in progress.`] : []
1418
1465
  ];
1419
1466
  };
@@ -3158,7 +3205,8 @@ const FEATURE_REQUIRES_SERVER_NEWER_THAN = {
3158
3205
  deliveryMetricsHistory: "v26.5.29.5",
3159
3206
  sleRisk: "v26.9.19.10",
3160
3207
  teamRefinement: "v26.10.3.6",
3161
- refinementVotes: "v26.10.3.6"
3208
+ refinementVotes: "v26.10.3.6",
3209
+ staleness: "v26.10.16.6"
3162
3210
  };
3163
3211
  /**
3164
3212
  * Parses a Lighthouse version string ("v26.5.24.10", "26.5.24") into numeric
@@ -3615,4 +3663,4 @@ const usageDataCalls = (configuration, dependencies) => {
3615
3663
  };
3616
3664
  };
3617
3665
  //#endregion
3618
- export { BLACKOUT_RULE_LIST_HEADINGS, BLACKOUT_RULE_LIST_TITLE, CANNOT_FORECAST_SHORT, FEATURE_REQUIRES_SERVER_NEWER_THAN, INSUFFICIENT_FORECAST_DATA_SENTENCE, INSUFFICIENT_FORECAST_DATA_SHORT, LIGHTHOUSE_IS_REACHABLE, NOT_SENT, NOT_SET, NO_DATA_YET, OVERDUE_SHORT, OVER_TIME_EMPTY_SENTENCE, PREDICTABILITY_SCORE_EXPLANATION, REFINEMENT_ANSWERS, SECRET_NOT_SHOWN, SEEDED_TERMS, SLE_RISK_AT_RISK_FROM, STANDALONE_VOTER_KEY_SCOPE, UNKNOWN_SHAPE_NOTE, USAGE_DATA_BUDGET_MS, VOTER_KEY_HEADER, WORK_TRACKING_CONNECTION_LIST_HEADINGS, WORK_TRACKING_OPTION_HEADINGS, countNumberedRows, createFileUsageDataStore, createFileVoterKeyStore, createLighthouseAuthContext, createLighthouseClient, daysInRange, deliveryLikelihoodAnswer, describeAnswer, describeArchivedDeliveriesTitle, describeArchivedDeliveryHeadings, describeArchivedDeliveryRow, describeArrivalsDays, describeAsOfHeading, describeBacktestActual, describeBacktestPeriod, describeBacktestSummary, describeBlackoutRuleCount, describeBlackoutRuleSchedule, describeBlackoutRuleWriteConfirmation, describeBlockedDays, describeBlockedNow, describeBlockedOverTime, describeConnectionName, describeConnectionSummary, describeConnectionType, describeCycleTimeDays, describeDays, describeDeliveryChanceHeadings, describeDeliveryChanceRow, describeDeliveryCount, describeDeliveryDone, describeDeliveryFeatureHeadings, describeDeliveryFeatureRow, describeDeliveryLikelihood, describeDeliveryListHeadings, describeDeliveryListTitle, describeDeliveryMetricsHeading, describeDeliveryRow, describeFeatureCompletion, describeFeatureCount, describeFeatureListCount, describeFeatureListHeadings, describeFeatureProgress, describeFeatureRow, describeFeatureStart, describeFeatureTitle, describeFeatureWorkItemHeadings, describeFeatureWorkItemRow, describeFeatureWorkItemsHeading, describeHowManyTitle, describeInProgressNow, describeLastUpdated, describeManualForecastLikelihood, describeManualForecastSummary, describeMetricSummary, describeMetricsHeading, describeMissingCondition, describeNoBlackoutRules, describeNoDeliveries, describeNothingToTakeBack, describeOptionValue, describeOverTimeHeading, describeOwnerCount, describeOwnerListHeadings, describeOwnerListTitle, describeOwnerName, describeOwnerWriteConfirmation, describePercentileRows, describePercentilesOverTime, describePercentilesOverTimeDays, describePortfolioSummary, describePredictabilityScore, describePredictabilityScoreDays, describeProcessBehaviorChart, describeProcessBehaviorOverTime, describeProcessBehaviorOverTimeDays, describeReachable, describeReadiness, describeRecordedComment, describeRecordedDayHeadings, describeRecordedDayRow, describeRecordedDayTitle, describeRecordedVote, describeRefinementSummary, describeRefreshConfirmation, describeRefusedMetric, describeSleRiskNow, describeTags, describeTakenBack, describeTeamSummary, describeThroughputDays, describeTimeInState, describeTimeInStateContributorDays, describeTimeInStateDays, describeTotalArrivals, describeTotalThroughput, describeTotalWorkItemAge, describeTotalWorkItemAgeDays, describeUnknownMetric, describeVersion, describeVoteRefusal, describeVotes, describeWarnings, describeWhatWipLeavesUnsaid, describeWhenTitle, describeWipDays, describeWorkItemAgeDays, describeWorkItemAgePercentiles, describeWorkTrackingSystemCount, formatCalendarDay, formatLikelihood, formatTimestamp, getClientPackageContract, getDefaultMetricsDateRange, getStandaloneDiscoveryLockfilePath, getUsageDataStorePath, getVoterKeyScope, getVoterKeyStorePath, hideConnectionSecrets, isAPersonAtTheTerminal, isAtRiskOfMissingTheSle, isCalendarDay, isDoNotTrackSet, isMetricRefusal, isMissingItsCondition, isProcessBehaviorMetricType, isRefinementAnswer, isServerVersionNewerThan, keepVoterKey, latestRecordedDay, levelOf, likelihoodAnswer, loadStandaloneDiscoveryContract, metricsHeadlineLabel, mintVoterKey, myVoteOn, nameOrFallback, ordinalOf, parseStandaloneDiscoveryContract, placeActualAmongPercentiles, placeEnoughForLine, planUsageDataStep, pollCliAuthSession, processBehaviorChartTitle, queryServerAuthMode, readAnswerWording, readArchivedDeliveryItem, readArrivals, readBacktest, readBlackoutRules, readBlocked, readCumulativeStateTime, readCycleTime, readCycleTimeDefinitionName, readCycleTimePercentiles, readDeliveryList, readDeliveryMetricsHistory, readFeatureList, readFeatureWording, readFeatureWorkItems, readInProgressItems, readManualForecast, readMetricAnswer, readMetricsSubject, readNameOfMyVote, readOwnerList, readPercentilesOverTime, readPortfolio, readPortfolioDeliveries, readPredictabilityScore, readProcessBehaviorChart, readProcessBehaviorOverTime, readRefinementTerms, readRefinementWording, readRunChart, readServiceLevelExpectation, readSleRisk, readStoredUsageDataAnswer, readSystemWipLimit, readTeam, readTerms, readThroughput, readTimeInStateBar, readTimeInStateContributors, readTotalWorkItemAge, readUsageDataState, readVoteRefusal, readWip, readWorkItemAge, readWorkItemAgePercentiles, readWorkTrackingConnection, readWorkTrackingConnections, readWrittenBlackoutRule, readWrittenOwner, refinementDayVerdictOf, resolveTerms, settleUsageDataStep, sizingMomentOf, startCliAuthSession, summariseDeliveryMetricsHistory, switchUsageDataOn, timeInStateItemCount, usageDataStoreFor, validateLighthouseConnectivity, voterKeyStoreFor, withdrawUsageData };
3666
+ export { BLACKOUT_RULE_LIST_HEADINGS, BLACKOUT_RULE_LIST_TITLE, CANNOT_FORECAST_SHORT, FEATURE_REQUIRES_SERVER_NEWER_THAN, INSUFFICIENT_FORECAST_DATA_SENTENCE, INSUFFICIENT_FORECAST_DATA_SHORT, LIGHTHOUSE_IS_REACHABLE, NOT_SENT, NOT_SET, NO_DATA_YET, OVERDUE_SHORT, OVER_TIME_EMPTY_SENTENCE, PREDICTABILITY_SCORE_EXPLANATION, REFINEMENT_ANSWERS, SECRET_NOT_SHOWN, SEEDED_TERMS, SLE_RISK_AT_RISK_FROM, STANDALONE_VOTER_KEY_SCOPE, UNKNOWN_SHAPE_NOTE, USAGE_DATA_BUDGET_MS, VOTER_KEY_HEADER, WORK_TRACKING_CONNECTION_LIST_HEADINGS, WORK_TRACKING_OPTION_HEADINGS, countNumberedRows, createFileUsageDataStore, createFileVoterKeyStore, createLighthouseAuthContext, createLighthouseClient, daysInRange, deliveryLikelihoodAnswer, describeAnswer, describeArchivedDeliveriesTitle, describeArchivedDeliveryHeadings, describeArchivedDeliveryRow, describeArrivalsDays, describeAsOfHeading, describeBacktestActual, describeBacktestPeriod, describeBacktestSummary, describeBlackoutRuleCount, describeBlackoutRuleSchedule, describeBlackoutRuleWriteConfirmation, describeBlockedDays, describeBlockedNow, describeBlockedOverTime, describeConnectionName, describeConnectionSummary, describeConnectionType, describeCycleTimeDays, describeDays, describeDeliveryChanceHeadings, describeDeliveryChanceRow, describeDeliveryCount, describeDeliveryDone, describeDeliveryFeatureHeadings, describeDeliveryFeatureRow, describeDeliveryLikelihood, describeDeliveryListHeadings, describeDeliveryListTitle, describeDeliveryMetricsHeading, describeDeliveryRow, describeFeatureCompletion, describeFeatureCount, describeFeatureListCount, describeFeatureListHeadings, describeFeatureProgress, describeFeatureRow, describeFeatureStart, describeFeatureTitle, describeFeatureWorkItemHeadings, describeFeatureWorkItemRow, describeFeatureWorkItemsHeading, describeHowManyTitle, describeInProgressNow, describeLastUpdated, describeManualForecastLikelihood, describeManualForecastSummary, describeMetricSummary, describeMetricsHeading, describeMissingCondition, describeNoBlackoutRules, describeNoDeliveries, describeNothingToTakeBack, describeOptionValue, describeOverTimeHeading, describeOwnerCount, describeOwnerListHeadings, describeOwnerListTitle, describeOwnerName, describeOwnerWriteConfirmation, describePercentileDate, describePercentileDay, describePercentileRows, describePercentilesOverTime, describePercentilesOverTimeDays, describePortfolioSummary, describePredictabilityScore, describePredictabilityScoreDays, describeProcessBehaviorChart, describeProcessBehaviorOverTime, describeProcessBehaviorOverTimeDays, describeReachable, describeReadiness, describeRecordedComment, describeRecordedDayHeadings, describeRecordedDayRow, describeRecordedDayTitle, describeRecordedVote, describeRefinementSummary, describeRefreshConfirmation, describeRefusedMetric, describeSleRiskNow, describeStaleNow, describeTags, describeTakenBack, describeTeamSummary, describeThroughputDays, describeTimeInState, describeTimeInStateContributorDays, describeTimeInStateDays, describeTotalArrivals, describeTotalThroughput, describeTotalWorkItemAge, describeTotalWorkItemAgeDays, describeUnknownMetric, describeVersion, describeVoteRefusal, describeVotes, describeWarnings, describeWhatWipLeavesUnsaid, describeWhenTitle, describeWipDays, describeWorkItemAgeDays, describeWorkItemAgePercentiles, describeWorkTrackingSystemCount, formatCalendarDay, formatLikelihood, formatTimestamp, getClientPackageContract, getDefaultMetricsDateRange, getStandaloneDiscoveryLockfilePath, getUsageDataStorePath, getVoterKeyScope, getVoterKeyStorePath, hideConnectionSecrets, isAPersonAtTheTerminal, isAtRiskOfMissingTheSle, isCalendarDay, isDoNotTrackSet, isMetricRefusal, isMissingItsCondition, isProcessBehaviorMetricType, isRefinementAnswer, isServerVersionNewerThan, keepVoterKey, latestRecordedDay, levelOf, likelihoodAnswer, loadStandaloneDiscoveryContract, metricsHeadlineLabel, mintVoterKey, moreThanYearsText, myVoteOn, nameOrFallback, ordinalOf, parseStandaloneDiscoveryContract, placeActualAmongPercentiles, placeEnoughForLine, planUsageDataStep, pollCliAuthSession, processBehaviorChartTitle, queryServerAuthMode, readAnswerWording, readArchivedDeliveryItem, readArrivals, readBacktest, readBlackoutRules, readBlocked, readCumulativeStateTime, readCycleTime, readCycleTimeDefinitionName, readCycleTimePercentiles, readDeliveryList, readDeliveryMetricsHistory, readFeatureList, readFeatureWording, readFeatureWorkItems, readInProgressItems, readManualForecast, readMetricAnswer, readMetricsSubject, readNameOfMyVote, readOwnerList, readPercentilesOverTime, readPortfolio, readPortfolioDeliveries, readPredictabilityScore, readProcessBehaviorChart, readProcessBehaviorOverTime, readRefinementTerms, readRefinementWording, readRunChart, readServiceLevelExpectation, readSleRisk, readStoredUsageDataAnswer, readSystemWipLimit, readTeam, readTerms, readThroughput, readTimeInStateBar, readTimeInStateContributors, readTotalWorkItemAge, readUsageDataState, readVoteRefusal, readWip, readWorkItemAge, readWorkItemAgePercentiles, readWorkTrackingConnection, readWorkTrackingConnections, readWrittenBlackoutRule, readWrittenOwner, refinementDayVerdictOf, resolveTerms, settleUsageDataStep, sizingMomentOf, startCliAuthSession, summariseDeliveryMetricsHistory, switchUsageDataOn, timeInStateItemCount, usageDataStoreFor, validateLighthouseConnectivity, voterKeyStoreFor, withdrawUsageData, yearsBeyondHorizon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letpeoplework/lighthouse-client",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Shared Lighthouse API client contracts",
5
5
  "repository": {
6
6
  "type": "git",