@letpeoplework/lighthouse-client 1.9.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) => [
@@ -627,11 +647,13 @@ const readFeatureWording = (source, featureId) => readAnswerWording(source, {
627
647
  //#region src/deliveryWording.ts
628
648
  const LISTED_CHANCE = 85;
629
649
  const flagOf = (value) => typeof value === "boolean" ? value : void 0;
650
+ const teamsWithoutForecastIn = (value) => Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0;
630
651
  const readDeliveryListItem = (value) => {
631
652
  if (!isRecord(value) || typeof value.id !== "number" || typeof value.totalWork !== "number" || typeof value.remainingWork !== "number") return null;
632
653
  const name = textOf(value.name);
633
654
  const date = textOf(value.date);
634
655
  if (name === void 0 || date === void 0) return null;
656
+ const likely = chanceAt(value.completionDates, LISTED_CHANCE);
635
657
  return {
636
658
  id: value.id,
637
659
  name,
@@ -640,10 +662,11 @@ const readDeliveryListItem = (value) => {
640
662
  totalWork: value.totalWork,
641
663
  remainingWork: value.remainingWork,
642
664
  likelihood: typeof value.likelihoodPercentage === "number" ? value.likelihoodPercentage : null,
643
- cannotBeForecast: Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0,
665
+ cannotBeForecast: teamsWithoutForecastIn(value),
644
666
  hasSufficientData: flagOf(value.hasSufficientData),
645
667
  isOverdue: flagOf(value.isOverdue),
646
- likelyBy: expectedDateAt(value.completionDates, LISTED_CHANCE)
668
+ likelyBy: textOf(likely?.expectedDate),
669
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(likely)
647
670
  };
648
671
  };
649
672
  /**
@@ -655,6 +678,40 @@ const readDeliveryList = (value) => {
655
678
  const items = value.map(readDeliveryListItem);
656
679
  return items.every((item) => item !== null) ? items : null;
657
680
  };
681
+ const doneWorkOf = (value) => {
682
+ if (typeof value.doneWork === "number") return value.doneWork;
683
+ return typeof value.totalWork === "number" && typeof value.remainingWork === "number" ? value.totalWork - value.remainingWork : void 0;
684
+ };
685
+ /** One archived Delivery, or null when it comes without its id, name, dates or work. */
686
+ const readArchivedDeliveryItem = (value) => {
687
+ if (!isRecord(value) || typeof value.id !== "number" || typeof value.totalWork !== "number") return null;
688
+ const name = textOf(value.name);
689
+ const date = textOf(value.date);
690
+ const archivedOn = textOf(value.archivedOn);
691
+ const doneWork = doneWorkOf(value);
692
+ if (name === void 0 || date === void 0 || archivedOn === void 0 || doneWork === void 0) return null;
693
+ return {
694
+ id: value.id,
695
+ name,
696
+ date,
697
+ archivedOn,
698
+ totalWork: value.totalWork,
699
+ doneWork,
700
+ likelihood: typeof value.likelihoodPercentage === "number" ? value.likelihoodPercentage : null,
701
+ cannotBeForecast: teamsWithoutForecastIn(value),
702
+ hasSufficientData: flagOf(value.hasSufficientData)
703
+ };
704
+ };
705
+ /** A Portfolio's active and archived Deliveries, or null when either list or any row in it cannot be read. */
706
+ const readPortfolioDeliveries = (value) => {
707
+ if (!isRecord(value) || !Array.isArray(value.archived)) return null;
708
+ const active = readDeliveryList(value.active);
709
+ const archived = value.archived.map(readArchivedDeliveryItem);
710
+ return active !== null && archived.every((item) => item !== null) ? {
711
+ active,
712
+ archived
713
+ } : null;
714
+ };
658
715
  /** "Ocean Explorer · Deliveries", or the Portfolio by its id when its name could not be read. */
659
716
  const describeDeliveryListTitle = (owner, terms) => {
660
717
  return `${owner.name === void 0 ? `${terms.portfolio} [id: ${owner.id}]` : owner.name} · ${terms.deliveries}`;
@@ -693,8 +750,38 @@ const describeDeliveryRow = (delivery, terms) => [
693
750
  delivery.featureCount === void 0 ? "—" : String(delivery.featureCount),
694
751
  describeDeliveryDone(delivery, terms),
695
752
  describeDeliveryLikelihood(delivery),
696
- delivery.likelyBy === void 0 ? "—" : dayOf(delivery.likelyBy)
753
+ delivery.likelyBy === void 0 ? "—" : describePercentileDay(delivery.likelyBy, delivery.likelyByBeyondHorizonYears)
754
+ ];
755
+ /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
756
+ const describeArchivedDeliveriesTitle = (terms) => `Archived ${terms.deliveries}`;
757
+ /** The archived table's column headings: when it was due, when it closed, and what it reached. */
758
+ const describeArchivedDeliveryHeadings = (terms) => [
759
+ "Name",
760
+ `${terms.delivery} Date`,
761
+ "Archived On",
762
+ "Done",
763
+ "Likelihood"
697
764
  ];
765
+ /** One archived Delivery as a row of its table, in the order of its headings. */
766
+ const describeArchivedDeliveryRow = (delivery, terms) => {
767
+ const remainingWork = delivery.totalWork - delivery.doneWork;
768
+ return [
769
+ describeOwnerName(delivery),
770
+ dayOf(delivery.date),
771
+ dayOf(delivery.archivedOn),
772
+ describeDeliveryDone({
773
+ totalWork: delivery.totalWork,
774
+ remainingWork
775
+ }, terms),
776
+ likelihoodAnswer({
777
+ likelihood: delivery.likelihood,
778
+ cannotBeForecast: delivery.cannotBeForecast,
779
+ hasRemainingWork: remainingWork > 0,
780
+ hasSufficientData: delivery.hasSufficientData,
781
+ precision: "round"
782
+ })
783
+ ];
784
+ };
698
785
  const isNullableNumber = (value) => value === null || typeof value === "number";
699
786
  const isFeatureMetric = (value) => isRecord(value) && typeof value.referenceId === "string" && typeof value.name === "string" && typeof value.completion === "number" && isNullableNumber(value.likelihood);
700
787
  const isChance = (value) => isRecord(value) && typeof value.probability === "number" && textOf(value.expectedDate) !== void 0;
@@ -766,18 +853,23 @@ const describeDeliveryFeatureRow = (feature) => [
766
853
  ];
767
854
  /** The forecast distribution's column headings. */
768
855
  const describeDeliveryChanceHeadings = () => ["Chance", "Done by"];
769
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
770
- 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)];
771
858
  //#endregion
772
859
  //#region src/forecastWording.ts
773
- const optionalBoolean = (value) => {
860
+ const optionalBoolean$1 = (value) => {
774
861
  if (value === void 0 || typeof value === "boolean") return value;
775
862
  return null;
776
863
  };
777
- const readChanceByDay = (value) => isRecord(value) && isNumber(value.probability) && isDay(value.expectedDate) ? {
778
- probability: value.probability,
779
- expectedDate: value.expectedDate
780
- } : 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
+ };
781
873
  const readChanceOfCount = (value) => isRecord(value) && isNumber(value.probability) && isNumber(value.value) ? {
782
874
  probability: value.probability,
783
875
  value: value.value
@@ -793,8 +885,8 @@ const readManualForecast = (value) => {
793
885
  const { remainingItems, targetDate, likelihood } = value;
794
886
  const whenForecasts = readEvery$1(value.whenForecasts, readChanceByDay);
795
887
  const howManyForecasts = readEvery$1(value.howManyForecasts, readChanceOfCount);
796
- const filterApplied = optionalBoolean(value.filterApplied);
797
- const hasSufficientData = optionalBoolean(value.hasSufficientData);
888
+ const filterApplied = optionalBoolean$1(value.filterApplied);
889
+ const hasSufficientData = optionalBoolean$1(value.hasSufficientData);
798
890
  if (!isNumber(remainingItems) || !(targetDate === null || isDay(targetDate)) || !(likelihood === null || isNumber(likelihood)) || whenForecasts === null || howManyForecasts === null || filterApplied === null || hasSufficientData === null) return null;
799
891
  return {
800
892
  remainingItems,
@@ -1081,13 +1173,28 @@ const readRunChart = (value, range) => {
1081
1173
  const optionalText = (value) => isText(value) ? value : void 0;
1082
1174
  const optionalNumber = (value) => isNumber(value) ? value : void 0;
1083
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;
1084
1189
  const readInProgressItem = (value) => isRecord(value) && (value.isBlocked === void 0 || typeof value.isBlocked === "boolean") ? {
1085
1190
  isBlocked: value.isBlocked,
1086
1191
  referenceId: optionalText(value.referenceId),
1087
1192
  name: optionalText(value.name),
1088
1193
  state: optionalText(value.state),
1089
1194
  workItemAge: optionalNumber(value.workItemAge),
1090
- blockedSince: optionalDay(value.blockedSince)
1195
+ blockedSince: optionalDay(value.blockedSince),
1196
+ isStale: optionalBoolean(value.isStale),
1197
+ staleReasons: optionalStaleReasons(value.staleReasons)
1091
1198
  } : null;
1092
1199
  const readInProgressNow = (value) => {
1093
1200
  if (!isRecord(value) || !isDay(value.asOfDate) || !isNumber(value.count)) return null;
@@ -1286,6 +1393,7 @@ const metricsHeadlineLabel = (part, scope, terms) => {
1286
1393
  throughput: `Total ${terms.throughput}`,
1287
1394
  arrivals: "Total Arrivals",
1288
1395
  blocked: `${terms.blocked} ${counted.many}`,
1396
+ stale: `Stale ${counted.many}`,
1289
1397
  totalWorkItemAge: `Total ${terms.workItemAge}`,
1290
1398
  predictabilityScore: "Predictability Score",
1291
1399
  cycleTimePercentiles: `${terms.cycleTime} percentiles`,
@@ -1329,27 +1437,31 @@ const describeInProgressNow = (now, scope, terms, systemWipLimit) => ({
1329
1437
  value: String(now.count),
1330
1438
  detail: systemWipLimit === void 0 ? "" : wipLimitOf(systemWipLimit, scope, terms)
1331
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;
1332
1448
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
1333
- const describeBlockedNow = (now, scope, terms) => {
1334
- if (!now.items.some((item) => item.isBlocked !== void 0)) return null;
1335
- const blocked = now.items.filter((item) => item.isBlocked === true).length;
1336
- return {
1337
- label: metricsHeadlineLabel("blocked", scope, terms),
1338
- value: String(blocked),
1339
- detail: ""
1340
- };
1341
- };
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);
1342
1452
  /**
1343
1453
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
1344
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
1345
- * 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.
1346
1457
  */
1347
1458
  const describeWhatWipLeavesUnsaid = (now, scope, terms, systemWipLimit) => {
1348
1459
  const counted = countedOf(scope, terms);
1349
- 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);
1350
1461
  return [
1351
1462
  ...systemWipLimit === void 0 ? [`No System ${terms.wip} Limit is set.`] : [],
1352
- ...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.`] : [],
1353
1465
  ...now.items.length === 0 ? [`No ${counted.many} are in progress.`] : []
1354
1466
  ];
1355
1467
  };
@@ -2796,6 +2908,16 @@ const pollCliAuthSession = async (endpointUrl, sessionId, dependencies) => {
2796
2908
  return { status: "expired" };
2797
2909
  }
2798
2910
  };
2911
+ const toPortfolioDeliveries = (value) => {
2912
+ if (Array.isArray(value)) return {
2913
+ active: value,
2914
+ archived: []
2915
+ };
2916
+ return isObjectRecord(value) && Array.isArray(value.active) && Array.isArray(value.archived) ? {
2917
+ active: value.active,
2918
+ archived: value.archived
2919
+ } : null;
2920
+ };
2799
2921
  const getDefaultMetricsDateRange = () => {
2800
2922
  const end = /* @__PURE__ */ new Date();
2801
2923
  const start = /* @__PURE__ */ new Date();
@@ -3084,7 +3206,8 @@ const FEATURE_REQUIRES_SERVER_NEWER_THAN = {
3084
3206
  deliveryMetricsHistory: "v26.5.29.5",
3085
3207
  sleRisk: "v26.9.19.10",
3086
3208
  teamRefinement: "v26.10.3.6",
3087
- refinementVotes: "v26.10.3.6"
3209
+ refinementVotes: "v26.10.3.6",
3210
+ staleness: "v26.10.16.6"
3088
3211
  };
3089
3212
  /**
3090
3213
  * Parses a Lighthouse version string ("v26.5.24.10", "26.5.24") into numeric
@@ -3431,7 +3554,18 @@ const createLighthouseClient = (configuration, dependencies = {}) => {
3431
3554
  return requestJson(configuration, dependencies, `/v1/features/references?${queryString}`, { method: "GET" });
3432
3555
  },
3433
3556
  getFeatureWorkItems: async (featureId) => requestJson(configuration, dependencies, `/v1/features/${featureId}/workitems`, { method: "GET" }),
3434
- listDeliveries: async (portfolioId) => requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, { method: "GET" }),
3557
+ listDeliveries: async (portfolioId) => {
3558
+ const result = await requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, { method: "GET" });
3559
+ if (!result.ok) return result;
3560
+ const deliveries = toPortfolioDeliveries(result.value);
3561
+ return deliveries === null ? getErrorResult({
3562
+ category: "unexpected",
3563
+ reason: "Lighthouse's list of Deliveries is not readable."
3564
+ }) : {
3565
+ ok: true,
3566
+ value: deliveries
3567
+ };
3568
+ },
3435
3569
  createDelivery: async (portfolioId, payload) => requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, {
3436
3570
  method: "POST",
3437
3571
  body: payload
@@ -3561,6 +3695,9 @@ exports.createLighthouseClient = createLighthouseClient;
3561
3695
  exports.daysInRange = daysInRange;
3562
3696
  exports.deliveryLikelihoodAnswer = deliveryLikelihoodAnswer;
3563
3697
  exports.describeAnswer = describeAnswer;
3698
+ exports.describeArchivedDeliveriesTitle = describeArchivedDeliveriesTitle;
3699
+ exports.describeArchivedDeliveryHeadings = describeArchivedDeliveryHeadings;
3700
+ exports.describeArchivedDeliveryRow = describeArchivedDeliveryRow;
3564
3701
  exports.describeArrivalsDays = describeArrivalsDays;
3565
3702
  exports.describeAsOfHeading = describeAsOfHeading;
3566
3703
  exports.describeBacktestActual = describeBacktestActual;
@@ -3617,6 +3754,8 @@ exports.describeOwnerListHeadings = describeOwnerListHeadings;
3617
3754
  exports.describeOwnerListTitle = describeOwnerListTitle;
3618
3755
  exports.describeOwnerName = describeOwnerName;
3619
3756
  exports.describeOwnerWriteConfirmation = describeOwnerWriteConfirmation;
3757
+ exports.describePercentileDate = describePercentileDate;
3758
+ exports.describePercentileDay = describePercentileDay;
3620
3759
  exports.describePercentileRows = describePercentileRows;
3621
3760
  exports.describePercentilesOverTime = describePercentilesOverTime;
3622
3761
  exports.describePercentilesOverTimeDays = describePercentilesOverTimeDays;
@@ -3637,6 +3776,7 @@ exports.describeRefinementSummary = describeRefinementSummary;
3637
3776
  exports.describeRefreshConfirmation = describeRefreshConfirmation;
3638
3777
  exports.describeRefusedMetric = describeRefusedMetric;
3639
3778
  exports.describeSleRiskNow = describeSleRiskNow;
3779
+ exports.describeStaleNow = describeStaleNow;
3640
3780
  exports.describeTags = describeTags;
3641
3781
  exports.describeTakenBack = describeTakenBack;
3642
3782
  exports.describeTeamSummary = describeTeamSummary;
@@ -3685,6 +3825,7 @@ exports.likelihoodAnswer = likelihoodAnswer;
3685
3825
  exports.loadStandaloneDiscoveryContract = loadStandaloneDiscoveryContract;
3686
3826
  exports.metricsHeadlineLabel = metricsHeadlineLabel;
3687
3827
  exports.mintVoterKey = mintVoterKey;
3828
+ exports.moreThanYearsText = moreThanYearsText;
3688
3829
  exports.myVoteOn = myVoteOn;
3689
3830
  exports.nameOrFallback = nameOrFallback;
3690
3831
  exports.ordinalOf = ordinalOf;
@@ -3696,6 +3837,7 @@ exports.pollCliAuthSession = pollCliAuthSession;
3696
3837
  exports.processBehaviorChartTitle = processBehaviorChartTitle;
3697
3838
  exports.queryServerAuthMode = queryServerAuthMode;
3698
3839
  exports.readAnswerWording = readAnswerWording;
3840
+ exports.readArchivedDeliveryItem = readArchivedDeliveryItem;
3699
3841
  exports.readArrivals = readArrivals;
3700
3842
  exports.readBacktest = readBacktest;
3701
3843
  exports.readBlackoutRules = readBlackoutRules;
@@ -3717,6 +3859,7 @@ exports.readNameOfMyVote = readNameOfMyVote;
3717
3859
  exports.readOwnerList = readOwnerList;
3718
3860
  exports.readPercentilesOverTime = readPercentilesOverTime;
3719
3861
  exports.readPortfolio = readPortfolio;
3862
+ exports.readPortfolioDeliveries = readPortfolioDeliveries;
3720
3863
  exports.readPredictabilityScore = readPredictabilityScore;
3721
3864
  exports.readProcessBehaviorChart = readProcessBehaviorChart;
3722
3865
  exports.readProcessBehaviorOverTime = readProcessBehaviorOverTime;
@@ -3754,3 +3897,4 @@ exports.usageDataStoreFor = usageDataStoreFor;
3754
3897
  exports.validateLighthouseConnectivity = validateLighthouseConnectivity;
3755
3898
  exports.voterKeyStoreFor = voterKeyStoreFor;
3756
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 = {
@@ -206,6 +207,25 @@ export type DeliveryListOwner = {
206
207
  * id, name, date or work: a row that cannot say what it is or when it is due would misstate the list.
207
208
  */
208
209
  export declare const readDeliveryList: (value: unknown) => DeliveryListItem[] | null;
210
+ /** One archived Delivery as the archived table states it: the numbers written down the day it closed. */
211
+ export type ArchivedDeliveryItem = {
212
+ readonly id: number;
213
+ readonly name: string;
214
+ readonly date: string;
215
+ readonly archivedOn: string;
216
+ readonly totalWork: number;
217
+ readonly doneWork: number;
218
+ readonly likelihood: number | null;
219
+ readonly cannotBeForecast: boolean;
220
+ readonly hasSufficientData: boolean | undefined;
221
+ };
222
+ /** One archived Delivery, or null when it comes without its id, name, dates or work. */
223
+ export declare const readArchivedDeliveryItem: (value: unknown) => ArchivedDeliveryItem | null;
224
+ /** A Portfolio's active and archived Deliveries, or null when either list or any row in it cannot be read. */
225
+ export declare const readPortfolioDeliveries: (value: unknown) => {
226
+ active: DeliveryListItem[];
227
+ archived: ArchivedDeliveryItem[];
228
+ } | null;
209
229
  /** "Ocean Explorer · Deliveries", or the Portfolio by its id when its name could not be read. */
210
230
  export declare const describeDeliveryListTitle: (owner: DeliveryListOwner, terms: Terms) => string;
211
231
  /** What a Portfolio without any Delivery shows instead of the table. */
@@ -220,6 +240,12 @@ export declare const describeDeliveryDone: (delivery: Pick<DeliveryListItem, "to
220
240
  export declare const describeDeliveryLikelihood: (delivery: DeliveryListItem) => string;
221
241
  /** One Delivery as a row of the list, in the order of its headings. */
222
242
  export declare const describeDeliveryRow: (delivery: DeliveryListItem, terms: Terms) => string[];
243
+ /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
244
+ export declare const describeArchivedDeliveriesTitle: (terms: Terms) => string;
245
+ /** The archived table's column headings: when it was due, when it closed, and what it reached. */
246
+ export declare const describeArchivedDeliveryHeadings: (terms: Terms) => string[];
247
+ /** One archived Delivery as a row of its table, in the order of its headings. */
248
+ export declare const describeArchivedDeliveryRow: (delivery: ArchivedDeliveryItem, terms: Terms) => string[];
223
249
  /**
224
250
  * A Delivery's recorded days, or null when the answer lacks its Delivery Date or any day lacks a fact its
225
251
  * row or detail states: a table with a hole in it would misstate the trend.
@@ -244,7 +270,7 @@ export declare const describeDeliveryFeatureHeadings: (terms: Terms) => string[]
244
270
  export declare const describeDeliveryFeatureRow: (feature: DeliveryFeatureMetric) => string[];
245
271
  /** The forecast distribution's column headings. */
246
272
  export declare const describeDeliveryChanceHeadings: () => string[];
247
- /** 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". */
248
274
  export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributionPoint) => string[];
249
275
  //#endregion
250
276
  //#region src/featureWording.d.ts
@@ -252,6 +278,7 @@ export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributio
252
278
  export type FeatureStart = {
253
279
  readonly observedOn: string | undefined;
254
280
  readonly likelyBy: string | undefined;
281
+ readonly likelyByBeyondHorizonYears?: number;
255
282
  };
256
283
  /** One Feature as a row of the Feature list states it. */
257
284
  export type FeatureListItem = {
@@ -263,6 +290,7 @@ export type FeatureListItem = {
263
290
  readonly cannotBeForecast: boolean;
264
291
  readonly start: FeatureStart;
265
292
  readonly likelyBy: string | undefined;
293
+ readonly likelyByBeyondHorizonYears?: number;
266
294
  };
267
295
  /**
268
296
  * Every Feature in the answer, or null when it is not a list or any one of them comes without its
@@ -280,8 +308,11 @@ export declare const describeFeatureProgress: (feature: Pick<FeatureListItem, "t
280
308
  * Team without history; that Team only stops anyone saying when work will begin.
281
309
  */
282
310
  export declare const describeFeatureStart: (feature: Pick<FeatureListItem, "start" | "cannotBeForecast">) => string;
283
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
284
- 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;
285
316
  /** One Feature as a row of the list, in the order of its headings. */
286
317
  export declare const describeFeatureRow: (feature: FeatureListItem, terms: Terms) => string[];
287
318
  /** One Work Item of a Feature as the Feature's Work Items list states it. */
@@ -332,11 +363,33 @@ export declare const deliveryLikelihoodAnswer: (facts: Parameters<typeof likelih
332
363
  readonly isOverdue?: boolean;
333
364
  }) => string;
334
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
335
389
  //#region src/forecastWording.d.ts
336
390
  /** One row of the When table: the chance of being done by a calendar day. */
337
- export type ForecastChanceByDay = {
391
+ export type ForecastChanceByDay = ForecastPercentileDate & {
338
392
  readonly probability: number;
339
- readonly expectedDate: string;
340
393
  };
341
394
  /** One row of the How Many table: the chance of getting this many done. */
342
395
  export type ForecastChanceOfCount = {
@@ -507,7 +560,15 @@ export declare const readArrivals: typeof readDailyCountChart;
507
560
  * range's first day, read as the total and the count of each day. Without a total the days are summed.
508
561
  */
509
562
  export declare const readRunChart: (value: unknown, range: MetricsDateRange) => DailyCountChartView | null;
510
- /** 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. */
511
572
  export type InProgressItem = {
512
573
  readonly isBlocked: boolean | undefined;
513
574
  readonly referenceId?: string;
@@ -515,6 +576,8 @@ export type InProgressItem = {
515
576
  readonly state?: string;
516
577
  readonly workItemAge?: number;
517
578
  readonly blockedSince?: string;
579
+ readonly isStale?: boolean;
580
+ readonly staleReasons?: readonly StaleReason[];
518
581
  };
519
582
  export type InProgressNowView = {
520
583
  readonly asOfDate: string;
@@ -596,7 +659,7 @@ export declare const PREDICTABILITY_SCORE_EXPLANATION = "The predictability scor
596
659
  /** What an empty over-time chart says on the web. */
597
660
  export declare const OVER_TIME_EMPTY_SENTENCE = "Nothing to show for the selected range. Days appear here as Lighthouse records them.";
598
661
  /** Every part of the headline that can be refused or unreadable on its own. */
599
- 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";
600
663
  /** "1 day", "12 days". */
601
664
  export declare const describeDays: (days: number) => string;
602
665
  /** The label each part of the headline goes by, in the instance's words. */
@@ -615,10 +678,13 @@ export declare const describeAsOfHeading: (subject: Pick<MetricsSubject, "endDat
615
678
  export declare const describeInProgressNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined) => MetricLine;
616
679
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
617
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;
618
683
  /**
619
684
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
620
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
621
- * 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.
622
688
  */
623
689
  export declare const describeWhatWipLeavesUnsaid: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined | null) => readonly string[];
624
690
  /** "Total Throughput 31 1.0 / day". */
@@ -1303,6 +1369,11 @@ export type LighthouseApiResult<TValue> = {
1303
1369
  readonly error: LighthouseApiError;
1304
1370
  };
1305
1371
  export type LighthouseWritePayload = Readonly<Record<string, unknown>>;
1372
+ /** A Portfolio's Deliveries: the ones still running apart from the archived ones. */
1373
+ export type PortfolioDeliveries = {
1374
+ readonly active: readonly unknown[];
1375
+ readonly archived: readonly unknown[];
1376
+ };
1306
1377
  export type MetricsDateRange = {
1307
1378
  readonly startDate: string;
1308
1379
  readonly endDate: string;
@@ -1401,6 +1472,10 @@ export type DeliveryFeatureMetric = {
1401
1472
  export type DeliveryWhenDistributionPoint = {
1402
1473
  readonly probability: number;
1403
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;
1404
1479
  };
1405
1480
  /** A delivery as it stood on one recorded day. Dates are ISO strings. */
1406
1481
  export type DeliveryMetricsHistoryPoint = {
@@ -1773,7 +1848,7 @@ export type LighthouseClient = {
1773
1848
  readonly getFeaturesByIds: (ids: readonly number[]) => Promise<LighthouseApiResult<readonly unknown[]>>;
1774
1849
  readonly getFeaturesByReferences: (refs: readonly string[]) => Promise<LighthouseApiResult<readonly unknown[]>>;
1775
1850
  readonly getFeatureWorkItems: (featureId: number) => Promise<LighthouseApiResult<readonly unknown[]>>;
1776
- readonly listDeliveries: (portfolioId: number) => Promise<LighthouseApiResult<readonly unknown[]>>;
1851
+ readonly listDeliveries: (portfolioId: number) => Promise<LighthouseApiResult<PortfolioDeliveries>>;
1777
1852
  readonly createDelivery: (portfolioId: number, payload: LighthouseWritePayload) => Promise<LighthouseApiResult<unknown>>;
1778
1853
  readonly updateDelivery: (deliveryId: number, payload: LighthouseWritePayload) => Promise<LighthouseApiResult<unknown>>;
1779
1854
  readonly deleteDelivery: (deliveryId: number) => Promise<LighthouseApiResult<undefined>>;
@@ -1821,6 +1896,7 @@ export declare const FEATURE_REQUIRES_SERVER_NEWER_THAN: {
1821
1896
  readonly sleRisk: "v26.9.19.10";
1822
1897
  readonly teamRefinement: "v26.10.3.6";
1823
1898
  readonly refinementVotes: "v26.10.3.6";
1899
+ readonly staleness: "v26.10.16.6";
1824
1900
  };
1825
1901
  /**
1826
1902
  * Returns true when `candidate` is strictly newer than `baseline`, false when
@@ -1831,4 +1907,5 @@ export declare const isServerVersionNewerThan: (candidate: string, baseline: str
1831
1907
  /** The header a client sends its voter key in, so Lighthouse can tell its votes from anyone else's. */
1832
1908
  export declare const VOTER_KEY_HEADER = "X-Lighthouse-Voter-Key";
1833
1909
  export declare const createLighthouseClient: (configuration: LighthouseClientConfiguration, dependencies?: LighthouseClientDependencies) => LighthouseClient;
1834
- //#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 = {
@@ -206,6 +207,25 @@ export type DeliveryListOwner = {
206
207
  * id, name, date or work: a row that cannot say what it is or when it is due would misstate the list.
207
208
  */
208
209
  export declare const readDeliveryList: (value: unknown) => DeliveryListItem[] | null;
210
+ /** One archived Delivery as the archived table states it: the numbers written down the day it closed. */
211
+ export type ArchivedDeliveryItem = {
212
+ readonly id: number;
213
+ readonly name: string;
214
+ readonly date: string;
215
+ readonly archivedOn: string;
216
+ readonly totalWork: number;
217
+ readonly doneWork: number;
218
+ readonly likelihood: number | null;
219
+ readonly cannotBeForecast: boolean;
220
+ readonly hasSufficientData: boolean | undefined;
221
+ };
222
+ /** One archived Delivery, or null when it comes without its id, name, dates or work. */
223
+ export declare const readArchivedDeliveryItem: (value: unknown) => ArchivedDeliveryItem | null;
224
+ /** A Portfolio's active and archived Deliveries, or null when either list or any row in it cannot be read. */
225
+ export declare const readPortfolioDeliveries: (value: unknown) => {
226
+ active: DeliveryListItem[];
227
+ archived: ArchivedDeliveryItem[];
228
+ } | null;
209
229
  /** "Ocean Explorer · Deliveries", or the Portfolio by its id when its name could not be read. */
210
230
  export declare const describeDeliveryListTitle: (owner: DeliveryListOwner, terms: Terms) => string;
211
231
  /** What a Portfolio without any Delivery shows instead of the table. */
@@ -220,6 +240,12 @@ export declare const describeDeliveryDone: (delivery: Pick<DeliveryListItem, "to
220
240
  export declare const describeDeliveryLikelihood: (delivery: DeliveryListItem) => string;
221
241
  /** One Delivery as a row of the list, in the order of its headings. */
222
242
  export declare const describeDeliveryRow: (delivery: DeliveryListItem, terms: Terms) => string[];
243
+ /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
244
+ export declare const describeArchivedDeliveriesTitle: (terms: Terms) => string;
245
+ /** The archived table's column headings: when it was due, when it closed, and what it reached. */
246
+ export declare const describeArchivedDeliveryHeadings: (terms: Terms) => string[];
247
+ /** One archived Delivery as a row of its table, in the order of its headings. */
248
+ export declare const describeArchivedDeliveryRow: (delivery: ArchivedDeliveryItem, terms: Terms) => string[];
223
249
  /**
224
250
  * A Delivery's recorded days, or null when the answer lacks its Delivery Date or any day lacks a fact its
225
251
  * row or detail states: a table with a hole in it would misstate the trend.
@@ -244,7 +270,7 @@ export declare const describeDeliveryFeatureHeadings: (terms: Terms) => string[]
244
270
  export declare const describeDeliveryFeatureRow: (feature: DeliveryFeatureMetric) => string[];
245
271
  /** The forecast distribution's column headings. */
246
272
  export declare const describeDeliveryChanceHeadings: () => string[];
247
- /** 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". */
248
274
  export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributionPoint) => string[];
249
275
  //#endregion
250
276
  //#region src/featureWording.d.ts
@@ -252,6 +278,7 @@ export declare const describeDeliveryChanceRow: (chance: DeliveryWhenDistributio
252
278
  export type FeatureStart = {
253
279
  readonly observedOn: string | undefined;
254
280
  readonly likelyBy: string | undefined;
281
+ readonly likelyByBeyondHorizonYears?: number;
255
282
  };
256
283
  /** One Feature as a row of the Feature list states it. */
257
284
  export type FeatureListItem = {
@@ -263,6 +290,7 @@ export type FeatureListItem = {
263
290
  readonly cannotBeForecast: boolean;
264
291
  readonly start: FeatureStart;
265
292
  readonly likelyBy: string | undefined;
293
+ readonly likelyByBeyondHorizonYears?: number;
266
294
  };
267
295
  /**
268
296
  * Every Feature in the answer, or null when it is not a list or any one of them comes without its
@@ -280,8 +308,11 @@ export declare const describeFeatureProgress: (feature: Pick<FeatureListItem, "t
280
308
  * Team without history; that Team only stops anyone saying when work will begin.
281
309
  */
282
310
  export declare const describeFeatureStart: (feature: Pick<FeatureListItem, "start" | "cannotBeForecast">) => string;
283
- /** The day the Feature is 85% likely to be done by, "Cannot forecast", or the mark when none was sent. */
284
- 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;
285
316
  /** One Feature as a row of the list, in the order of its headings. */
286
317
  export declare const describeFeatureRow: (feature: FeatureListItem, terms: Terms) => string[];
287
318
  /** One Work Item of a Feature as the Feature's Work Items list states it. */
@@ -332,11 +363,33 @@ export declare const deliveryLikelihoodAnswer: (facts: Parameters<typeof likelih
332
363
  readonly isOverdue?: boolean;
333
364
  }) => string;
334
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
335
389
  //#region src/forecastWording.d.ts
336
390
  /** One row of the When table: the chance of being done by a calendar day. */
337
- export type ForecastChanceByDay = {
391
+ export type ForecastChanceByDay = ForecastPercentileDate & {
338
392
  readonly probability: number;
339
- readonly expectedDate: string;
340
393
  };
341
394
  /** One row of the How Many table: the chance of getting this many done. */
342
395
  export type ForecastChanceOfCount = {
@@ -507,7 +560,15 @@ export declare const readArrivals: typeof readDailyCountChart;
507
560
  * range's first day, read as the total and the count of each day. Without a total the days are summed.
508
561
  */
509
562
  export declare const readRunChart: (value: unknown, range: MetricsDateRange) => DailyCountChartView | null;
510
- /** 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. */
511
572
  export type InProgressItem = {
512
573
  readonly isBlocked: boolean | undefined;
513
574
  readonly referenceId?: string;
@@ -515,6 +576,8 @@ export type InProgressItem = {
515
576
  readonly state?: string;
516
577
  readonly workItemAge?: number;
517
578
  readonly blockedSince?: string;
579
+ readonly isStale?: boolean;
580
+ readonly staleReasons?: readonly StaleReason[];
518
581
  };
519
582
  export type InProgressNowView = {
520
583
  readonly asOfDate: string;
@@ -596,7 +659,7 @@ export declare const PREDICTABILITY_SCORE_EXPLANATION = "The predictability scor
596
659
  /** What an empty over-time chart says on the web. */
597
660
  export declare const OVER_TIME_EMPTY_SENTENCE = "Nothing to show for the selected range. Days appear here as Lighthouse records them.";
598
661
  /** Every part of the headline that can be refused or unreadable on its own. */
599
- 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";
600
663
  /** "1 day", "12 days". */
601
664
  export declare const describeDays: (days: number) => string;
602
665
  /** The label each part of the headline goes by, in the instance's words. */
@@ -615,10 +678,13 @@ export declare const describeAsOfHeading: (subject: Pick<MetricsSubject, "endDat
615
678
  export declare const describeInProgressNow: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined) => MetricLine;
616
679
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
617
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;
618
683
  /**
619
684
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
620
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
621
- * 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.
622
688
  */
623
689
  export declare const describeWhatWipLeavesUnsaid: (now: InProgressNowView, scope: MetricsScope, terms: Terms, systemWipLimit: number | undefined | null) => readonly string[];
624
690
  /** "Total Throughput 31 1.0 / day". */
@@ -1303,6 +1369,11 @@ export type LighthouseApiResult<TValue> = {
1303
1369
  readonly error: LighthouseApiError;
1304
1370
  };
1305
1371
  export type LighthouseWritePayload = Readonly<Record<string, unknown>>;
1372
+ /** A Portfolio's Deliveries: the ones still running apart from the archived ones. */
1373
+ export type PortfolioDeliveries = {
1374
+ readonly active: readonly unknown[];
1375
+ readonly archived: readonly unknown[];
1376
+ };
1306
1377
  export type MetricsDateRange = {
1307
1378
  readonly startDate: string;
1308
1379
  readonly endDate: string;
@@ -1401,6 +1472,10 @@ export type DeliveryFeatureMetric = {
1401
1472
  export type DeliveryWhenDistributionPoint = {
1402
1473
  readonly probability: number;
1403
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;
1404
1479
  };
1405
1480
  /** A delivery as it stood on one recorded day. Dates are ISO strings. */
1406
1481
  export type DeliveryMetricsHistoryPoint = {
@@ -1773,7 +1848,7 @@ export type LighthouseClient = {
1773
1848
  readonly getFeaturesByIds: (ids: readonly number[]) => Promise<LighthouseApiResult<readonly unknown[]>>;
1774
1849
  readonly getFeaturesByReferences: (refs: readonly string[]) => Promise<LighthouseApiResult<readonly unknown[]>>;
1775
1850
  readonly getFeatureWorkItems: (featureId: number) => Promise<LighthouseApiResult<readonly unknown[]>>;
1776
- readonly listDeliveries: (portfolioId: number) => Promise<LighthouseApiResult<readonly unknown[]>>;
1851
+ readonly listDeliveries: (portfolioId: number) => Promise<LighthouseApiResult<PortfolioDeliveries>>;
1777
1852
  readonly createDelivery: (portfolioId: number, payload: LighthouseWritePayload) => Promise<LighthouseApiResult<unknown>>;
1778
1853
  readonly updateDelivery: (deliveryId: number, payload: LighthouseWritePayload) => Promise<LighthouseApiResult<unknown>>;
1779
1854
  readonly deleteDelivery: (deliveryId: number) => Promise<LighthouseApiResult<undefined>>;
@@ -1821,6 +1896,7 @@ export declare const FEATURE_REQUIRES_SERVER_NEWER_THAN: {
1821
1896
  readonly sleRisk: "v26.9.19.10";
1822
1897
  readonly teamRefinement: "v26.10.3.6";
1823
1898
  readonly refinementVotes: "v26.10.3.6";
1899
+ readonly staleness: "v26.10.16.6";
1824
1900
  };
1825
1901
  /**
1826
1902
  * Returns true when `candidate` is strictly newer than `baseline`, false when
@@ -1831,4 +1907,5 @@ export declare const isServerVersionNewerThan: (candidate: string, baseline: str
1831
1907
  /** The header a client sends its voter key in, so Lighthouse can tell its votes from anyone else's. */
1832
1908
  export declare const VOTER_KEY_HEADER = "X-Lighthouse-Voter-Key";
1833
1909
  export declare const createLighthouseClient: (configuration: LighthouseClientConfiguration, dependencies?: LighthouseClientDependencies) => LighthouseClient;
1834
- //#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) => [
@@ -626,11 +646,13 @@ const readFeatureWording = (source, featureId) => readAnswerWording(source, {
626
646
  //#region src/deliveryWording.ts
627
647
  const LISTED_CHANCE = 85;
628
648
  const flagOf = (value) => typeof value === "boolean" ? value : void 0;
649
+ const teamsWithoutForecastIn = (value) => Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0;
629
650
  const readDeliveryListItem = (value) => {
630
651
  if (!isRecord(value) || typeof value.id !== "number" || typeof value.totalWork !== "number" || typeof value.remainingWork !== "number") return null;
631
652
  const name = textOf(value.name);
632
653
  const date = textOf(value.date);
633
654
  if (name === void 0 || date === void 0) return null;
655
+ const likely = chanceAt(value.completionDates, LISTED_CHANCE);
634
656
  return {
635
657
  id: value.id,
636
658
  name,
@@ -639,10 +661,11 @@ const readDeliveryListItem = (value) => {
639
661
  totalWork: value.totalWork,
640
662
  remainingWork: value.remainingWork,
641
663
  likelihood: typeof value.likelihoodPercentage === "number" ? value.likelihoodPercentage : null,
642
- cannotBeForecast: Array.isArray(value.teamsWithoutForecast) && value.teamsWithoutForecast.length > 0,
664
+ cannotBeForecast: teamsWithoutForecastIn(value),
643
665
  hasSufficientData: flagOf(value.hasSufficientData),
644
666
  isOverdue: flagOf(value.isOverdue),
645
- likelyBy: expectedDateAt(value.completionDates, LISTED_CHANCE)
667
+ likelyBy: textOf(likely?.expectedDate),
668
+ likelyByBeyondHorizonYears: yearsBeyondHorizon(likely)
646
669
  };
647
670
  };
648
671
  /**
@@ -654,6 +677,40 @@ const readDeliveryList = (value) => {
654
677
  const items = value.map(readDeliveryListItem);
655
678
  return items.every((item) => item !== null) ? items : null;
656
679
  };
680
+ const doneWorkOf = (value) => {
681
+ if (typeof value.doneWork === "number") return value.doneWork;
682
+ return typeof value.totalWork === "number" && typeof value.remainingWork === "number" ? value.totalWork - value.remainingWork : void 0;
683
+ };
684
+ /** One archived Delivery, or null when it comes without its id, name, dates or work. */
685
+ const readArchivedDeliveryItem = (value) => {
686
+ if (!isRecord(value) || typeof value.id !== "number" || typeof value.totalWork !== "number") return null;
687
+ const name = textOf(value.name);
688
+ const date = textOf(value.date);
689
+ const archivedOn = textOf(value.archivedOn);
690
+ const doneWork = doneWorkOf(value);
691
+ if (name === void 0 || date === void 0 || archivedOn === void 0 || doneWork === void 0) return null;
692
+ return {
693
+ id: value.id,
694
+ name,
695
+ date,
696
+ archivedOn,
697
+ totalWork: value.totalWork,
698
+ doneWork,
699
+ likelihood: typeof value.likelihoodPercentage === "number" ? value.likelihoodPercentage : null,
700
+ cannotBeForecast: teamsWithoutForecastIn(value),
701
+ hasSufficientData: flagOf(value.hasSufficientData)
702
+ };
703
+ };
704
+ /** A Portfolio's active and archived Deliveries, or null when either list or any row in it cannot be read. */
705
+ const readPortfolioDeliveries = (value) => {
706
+ if (!isRecord(value) || !Array.isArray(value.archived)) return null;
707
+ const active = readDeliveryList(value.active);
708
+ const archived = value.archived.map(readArchivedDeliveryItem);
709
+ return active !== null && archived.every((item) => item !== null) ? {
710
+ active,
711
+ archived
712
+ } : null;
713
+ };
657
714
  /** "Ocean Explorer · Deliveries", or the Portfolio by its id when its name could not be read. */
658
715
  const describeDeliveryListTitle = (owner, terms) => {
659
716
  return `${owner.name === void 0 ? `${terms.portfolio} [id: ${owner.id}]` : owner.name} · ${terms.deliveries}`;
@@ -692,8 +749,38 @@ const describeDeliveryRow = (delivery, terms) => [
692
749
  delivery.featureCount === void 0 ? "—" : String(delivery.featureCount),
693
750
  describeDeliveryDone(delivery, terms),
694
751
  describeDeliveryLikelihood(delivery),
695
- delivery.likelyBy === void 0 ? "—" : dayOf(delivery.likelyBy)
752
+ delivery.likelyBy === void 0 ? "—" : describePercentileDay(delivery.likelyBy, delivery.likelyByBeyondHorizonYears)
753
+ ];
754
+ /** What heads the archived Deliveries, below the active ones: "Archived Deliveries". */
755
+ const describeArchivedDeliveriesTitle = (terms) => `Archived ${terms.deliveries}`;
756
+ /** The archived table's column headings: when it was due, when it closed, and what it reached. */
757
+ const describeArchivedDeliveryHeadings = (terms) => [
758
+ "Name",
759
+ `${terms.delivery} Date`,
760
+ "Archived On",
761
+ "Done",
762
+ "Likelihood"
696
763
  ];
764
+ /** One archived Delivery as a row of its table, in the order of its headings. */
765
+ const describeArchivedDeliveryRow = (delivery, terms) => {
766
+ const remainingWork = delivery.totalWork - delivery.doneWork;
767
+ return [
768
+ describeOwnerName(delivery),
769
+ dayOf(delivery.date),
770
+ dayOf(delivery.archivedOn),
771
+ describeDeliveryDone({
772
+ totalWork: delivery.totalWork,
773
+ remainingWork
774
+ }, terms),
775
+ likelihoodAnswer({
776
+ likelihood: delivery.likelihood,
777
+ cannotBeForecast: delivery.cannotBeForecast,
778
+ hasRemainingWork: remainingWork > 0,
779
+ hasSufficientData: delivery.hasSufficientData,
780
+ precision: "round"
781
+ })
782
+ ];
783
+ };
697
784
  const isNullableNumber = (value) => value === null || typeof value === "number";
698
785
  const isFeatureMetric = (value) => isRecord(value) && typeof value.referenceId === "string" && typeof value.name === "string" && typeof value.completion === "number" && isNullableNumber(value.likelihood);
699
786
  const isChance = (value) => isRecord(value) && typeof value.probability === "number" && textOf(value.expectedDate) !== void 0;
@@ -765,18 +852,23 @@ const describeDeliveryFeatureRow = (feature) => [
765
852
  ];
766
853
  /** The forecast distribution's column headings. */
767
854
  const describeDeliveryChanceHeadings = () => ["Chance", "Done by"];
768
- /** One chance of the day's forecast: "85%" and the day it is that likely to be done by. */
769
- 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)];
770
857
  //#endregion
771
858
  //#region src/forecastWording.ts
772
- const optionalBoolean = (value) => {
859
+ const optionalBoolean$1 = (value) => {
773
860
  if (value === void 0 || typeof value === "boolean") return value;
774
861
  return null;
775
862
  };
776
- const readChanceByDay = (value) => isRecord(value) && isNumber(value.probability) && isDay(value.expectedDate) ? {
777
- probability: value.probability,
778
- expectedDate: value.expectedDate
779
- } : 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
+ };
780
872
  const readChanceOfCount = (value) => isRecord(value) && isNumber(value.probability) && isNumber(value.value) ? {
781
873
  probability: value.probability,
782
874
  value: value.value
@@ -792,8 +884,8 @@ const readManualForecast = (value) => {
792
884
  const { remainingItems, targetDate, likelihood } = value;
793
885
  const whenForecasts = readEvery$1(value.whenForecasts, readChanceByDay);
794
886
  const howManyForecasts = readEvery$1(value.howManyForecasts, readChanceOfCount);
795
- const filterApplied = optionalBoolean(value.filterApplied);
796
- const hasSufficientData = optionalBoolean(value.hasSufficientData);
887
+ const filterApplied = optionalBoolean$1(value.filterApplied);
888
+ const hasSufficientData = optionalBoolean$1(value.hasSufficientData);
797
889
  if (!isNumber(remainingItems) || !(targetDate === null || isDay(targetDate)) || !(likelihood === null || isNumber(likelihood)) || whenForecasts === null || howManyForecasts === null || filterApplied === null || hasSufficientData === null) return null;
798
890
  return {
799
891
  remainingItems,
@@ -1080,13 +1172,28 @@ const readRunChart = (value, range) => {
1080
1172
  const optionalText = (value) => isText(value) ? value : void 0;
1081
1173
  const optionalNumber = (value) => isNumber(value) ? value : void 0;
1082
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;
1083
1188
  const readInProgressItem = (value) => isRecord(value) && (value.isBlocked === void 0 || typeof value.isBlocked === "boolean") ? {
1084
1189
  isBlocked: value.isBlocked,
1085
1190
  referenceId: optionalText(value.referenceId),
1086
1191
  name: optionalText(value.name),
1087
1192
  state: optionalText(value.state),
1088
1193
  workItemAge: optionalNumber(value.workItemAge),
1089
- blockedSince: optionalDay(value.blockedSince)
1194
+ blockedSince: optionalDay(value.blockedSince),
1195
+ isStale: optionalBoolean(value.isStale),
1196
+ staleReasons: optionalStaleReasons(value.staleReasons)
1090
1197
  } : null;
1091
1198
  const readInProgressNow = (value) => {
1092
1199
  if (!isRecord(value) || !isDay(value.asOfDate) || !isNumber(value.count)) return null;
@@ -1285,6 +1392,7 @@ const metricsHeadlineLabel = (part, scope, terms) => {
1285
1392
  throughput: `Total ${terms.throughput}`,
1286
1393
  arrivals: "Total Arrivals",
1287
1394
  blocked: `${terms.blocked} ${counted.many}`,
1395
+ stale: `Stale ${counted.many}`,
1288
1396
  totalWorkItemAge: `Total ${terms.workItemAge}`,
1289
1397
  predictabilityScore: "Predictability Score",
1290
1398
  cycleTimePercentiles: `${terms.cycleTime} percentiles`,
@@ -1328,27 +1436,31 @@ const describeInProgressNow = (now, scope, terms, systemWipLimit) => ({
1328
1436
  value: String(now.count),
1329
1437
  detail: systemWipLimit === void 0 ? "" : wipLimitOf(systemWipLimit, scope, terms)
1330
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;
1331
1447
  /** "Blocked Work Items 2", or null when Lighthouse does not say which items are blocked. */
1332
- const describeBlockedNow = (now, scope, terms) => {
1333
- if (!now.items.some((item) => item.isBlocked !== void 0)) return null;
1334
- const blocked = now.items.filter((item) => item.isBlocked === true).length;
1335
- return {
1336
- label: metricsHeadlineLabel("blocked", scope, terms),
1337
- value: String(blocked),
1338
- detail: ""
1339
- };
1340
- };
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);
1341
1451
  /**
1342
1452
  * The sentences that stand in for what a WIP answer cannot show: no System WIP Limit, a Lighthouse that does
1343
- * not say which Work Items are Blocked (which is not the same as none being Blocked), and nothing in progress.
1344
- * 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.
1345
1456
  */
1346
1457
  const describeWhatWipLeavesUnsaid = (now, scope, terms, systemWipLimit) => {
1347
1458
  const counted = countedOf(scope, terms);
1348
- 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);
1349
1460
  return [
1350
1461
  ...systemWipLimit === void 0 ? [`No System ${terms.wip} Limit is set.`] : [],
1351
- ...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.`] : [],
1352
1464
  ...now.items.length === 0 ? [`No ${counted.many} are in progress.`] : []
1353
1465
  ];
1354
1466
  };
@@ -2795,6 +2907,16 @@ const pollCliAuthSession = async (endpointUrl, sessionId, dependencies) => {
2795
2907
  return { status: "expired" };
2796
2908
  }
2797
2909
  };
2910
+ const toPortfolioDeliveries = (value) => {
2911
+ if (Array.isArray(value)) return {
2912
+ active: value,
2913
+ archived: []
2914
+ };
2915
+ return isObjectRecord(value) && Array.isArray(value.active) && Array.isArray(value.archived) ? {
2916
+ active: value.active,
2917
+ archived: value.archived
2918
+ } : null;
2919
+ };
2798
2920
  const getDefaultMetricsDateRange = () => {
2799
2921
  const end = /* @__PURE__ */ new Date();
2800
2922
  const start = /* @__PURE__ */ new Date();
@@ -3083,7 +3205,8 @@ const FEATURE_REQUIRES_SERVER_NEWER_THAN = {
3083
3205
  deliveryMetricsHistory: "v26.5.29.5",
3084
3206
  sleRisk: "v26.9.19.10",
3085
3207
  teamRefinement: "v26.10.3.6",
3086
- refinementVotes: "v26.10.3.6"
3208
+ refinementVotes: "v26.10.3.6",
3209
+ staleness: "v26.10.16.6"
3087
3210
  };
3088
3211
  /**
3089
3212
  * Parses a Lighthouse version string ("v26.5.24.10", "26.5.24") into numeric
@@ -3430,7 +3553,18 @@ const createLighthouseClient = (configuration, dependencies = {}) => {
3430
3553
  return requestJson(configuration, dependencies, `/v1/features/references?${queryString}`, { method: "GET" });
3431
3554
  },
3432
3555
  getFeatureWorkItems: async (featureId) => requestJson(configuration, dependencies, `/v1/features/${featureId}/workitems`, { method: "GET" }),
3433
- listDeliveries: async (portfolioId) => requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, { method: "GET" }),
3556
+ listDeliveries: async (portfolioId) => {
3557
+ const result = await requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, { method: "GET" });
3558
+ if (!result.ok) return result;
3559
+ const deliveries = toPortfolioDeliveries(result.value);
3560
+ return deliveries === null ? getErrorResult({
3561
+ category: "unexpected",
3562
+ reason: "Lighthouse's list of Deliveries is not readable."
3563
+ }) : {
3564
+ ok: true,
3565
+ value: deliveries
3566
+ };
3567
+ },
3434
3568
  createDelivery: async (portfolioId, payload) => requestJson(configuration, dependencies, `/v1/deliveries/portfolio/${portfolioId}`, {
3435
3569
  method: "POST",
3436
3570
  body: payload
@@ -3529,4 +3663,4 @@ const usageDataCalls = (configuration, dependencies) => {
3529
3663
  };
3530
3664
  };
3531
3665
  //#endregion
3532
- 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, 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, readArrivals, readBacktest, readBlackoutRules, readBlocked, readCumulativeStateTime, readCycleTime, readCycleTimeDefinitionName, readCycleTimePercentiles, readDeliveryList, readDeliveryMetricsHistory, readFeatureList, readFeatureWording, readFeatureWorkItems, readInProgressItems, readManualForecast, readMetricAnswer, readMetricsSubject, readNameOfMyVote, readOwnerList, readPercentilesOverTime, readPortfolio, 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.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Shared Lighthouse API client contracts",
5
5
  "repository": {
6
6
  "type": "git",