@spottoai/types-package 1.0.2-beta.427 → 1.0.2-beta.429

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/azure/dataProtection.d.ts +40 -0
  2. package/dist/azure/dataProtection.d.ts.map +1 -1
  3. package/dist/azure/metrics.d.ts +2 -27
  4. package/dist/azure/metrics.d.ts.map +1 -1
  5. package/dist/azure/payloads.d.ts +7 -7
  6. package/dist/azure/payloads.d.ts.map +1 -1
  7. package/dist/azure/payloads.js +22 -23
  8. package/dist/azure/payloads.js.map +1 -1
  9. package/dist/azure/reportEvidence.d.ts +7 -5
  10. package/dist/azure/reportEvidence.d.ts.map +1 -1
  11. package/dist/azure/reportEvidence.js.map +1 -1
  12. package/dist/azure/reportEvidenceValidation.d.ts.map +1 -1
  13. package/dist/azure/reportEvidenceValidation.js +5 -1
  14. package/dist/azure/reportEvidenceValidation.js.map +1 -1
  15. package/dist/azure/reportEvidenceValidationHelpers.d.ts +3 -16
  16. package/dist/azure/reportEvidenceValidationHelpers.d.ts.map +1 -1
  17. package/dist/azure/reportEvidenceValidationHelpers.js +79 -95
  18. package/dist/azure/reportEvidenceValidationHelpers.js.map +1 -1
  19. package/dist/azure/resilienceFacts.d.ts +100 -0
  20. package/dist/azure/resilienceFacts.d.ts.map +1 -0
  21. package/dist/azure/resilienceFacts.js +20 -0
  22. package/dist/azure/resilienceFacts.js.map +1 -0
  23. package/dist/azure/resilienceFactsValidation.d.ts +4 -0
  24. package/dist/azure/resilienceFactsValidation.d.ts.map +1 -0
  25. package/dist/azure/resilienceFactsValidation.js +70 -0
  26. package/dist/azure/resilienceFactsValidation.js.map +1 -0
  27. package/dist/azure/views.d.ts +7 -0
  28. package/dist/azure/views.d.ts.map +1 -1
  29. package/dist/azure/views.js.map +1 -1
  30. package/dist/common/boundedRows.d.ts +7 -0
  31. package/dist/common/boundedRows.d.ts.map +1 -0
  32. package/dist/{scheduler/scheduler.js → common/boundedRows.js} +1 -1
  33. package/dist/common/boundedRows.js.map +1 -0
  34. package/dist/common/metricStats.d.ts +29 -0
  35. package/dist/common/metricStats.d.ts.map +1 -0
  36. package/dist/{scheduler/bastionSchedule.js → common/metricStats.js} +1 -1
  37. package/dist/common/metricStats.js.map +1 -0
  38. package/dist/common/utilizationStories.d.ts +516 -0
  39. package/dist/common/utilizationStories.d.ts.map +1 -0
  40. package/dist/common/utilizationStories.js +21 -0
  41. package/dist/common/utilizationStories.js.map +1 -0
  42. package/dist/common/utilizationStoriesValidation.d.ts +48 -0
  43. package/dist/common/utilizationStoriesValidation.d.ts.map +1 -0
  44. package/dist/common/utilizationStoriesValidation.js +552 -0
  45. package/dist/common/utilizationStoriesValidation.js.map +1 -0
  46. package/dist/common/validationHelpers.d.ts +18 -0
  47. package/dist/common/validationHelpers.d.ts.map +1 -0
  48. package/dist/common/validationHelpers.js +36 -0
  49. package/dist/common/validationHelpers.js.map +1 -0
  50. package/dist/esm/azure/payloads.js +19 -20
  51. package/dist/esm/azure/reportEvidenceValidation.js +5 -1
  52. package/dist/esm/azure/reportEvidenceValidationHelpers.js +2 -18
  53. package/dist/esm/azure/resilienceFacts.js +16 -0
  54. package/dist/esm/azure/resilienceFactsValidation.js +64 -0
  55. package/dist/esm/common/utilizationStories.js +17 -0
  56. package/dist/esm/common/utilizationStoriesValidation.js +514 -0
  57. package/dist/esm/common/validationHelpers.js +18 -0
  58. package/dist/esm/index.js +4 -3
  59. package/dist/esm/scheduler/index.js +2 -1
  60. package/dist/esm/scheduler/resourceStrategyCapabilityValidation.js +4 -20
  61. package/dist/esm/scheduler/resourceStrategyContracts.js +4 -2
  62. package/dist/esm/scheduler/resourceStrategyScheduleValidation.js +109 -4
  63. package/dist/esm/scheduler/schedulerContracts.js +1 -0
  64. package/dist/esm/scheduler/schedulerValidation.js +363 -0
  65. package/dist/index.d.ts +4 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -3
  68. package/dist/index.js.map +1 -1
  69. package/dist/scheduler/index.d.ts +2 -1
  70. package/dist/scheduler/index.d.ts.map +1 -1
  71. package/dist/scheduler/index.js +2 -1
  72. package/dist/scheduler/index.js.map +1 -1
  73. package/dist/scheduler/resourceStrategyCapabilityValidation.d.ts.map +1 -1
  74. package/dist/scheduler/resourceStrategyCapabilityValidation.js +4 -20
  75. package/dist/scheduler/resourceStrategyCapabilityValidation.js.map +1 -1
  76. package/dist/scheduler/resourceStrategyContracts.d.ts +26 -9
  77. package/dist/scheduler/resourceStrategyContracts.d.ts.map +1 -1
  78. package/dist/scheduler/resourceStrategyContracts.js +4 -2
  79. package/dist/scheduler/resourceStrategyContracts.js.map +1 -1
  80. package/dist/scheduler/resourceStrategyScheduleValidation.d.ts +3 -1
  81. package/dist/scheduler/resourceStrategyScheduleValidation.d.ts.map +1 -1
  82. package/dist/scheduler/resourceStrategyScheduleValidation.js +110 -4
  83. package/dist/scheduler/resourceStrategyScheduleValidation.js.map +1 -1
  84. package/dist/scheduler/schedulerContracts.d.ts +178 -0
  85. package/dist/scheduler/schedulerContracts.d.ts.map +1 -0
  86. package/dist/scheduler/schedulerContracts.js +3 -0
  87. package/dist/scheduler/schedulerContracts.js.map +1 -0
  88. package/dist/scheduler/schedulerValidation.d.ts +14 -0
  89. package/dist/scheduler/schedulerValidation.d.ts.map +1 -0
  90. package/dist/scheduler/schedulerValidation.js +378 -0
  91. package/dist/scheduler/schedulerValidation.js.map +1 -0
  92. package/package.json +5 -3
  93. package/dist/esm/scheduler/bastionScheduleProfile.js +0 -206
  94. package/dist/esm/scheduler/bastionScheduleValidation.js +0 -399
  95. package/dist/scheduler/bastionSchedule.d.ts +0 -113
  96. package/dist/scheduler/bastionSchedule.d.ts.map +0 -1
  97. package/dist/scheduler/bastionSchedule.js.map +0 -1
  98. package/dist/scheduler/bastionScheduleProfile.d.ts +0 -24
  99. package/dist/scheduler/bastionScheduleProfile.d.ts.map +0 -1
  100. package/dist/scheduler/bastionScheduleProfile.js +0 -210
  101. package/dist/scheduler/bastionScheduleProfile.js.map +0 -1
  102. package/dist/scheduler/bastionScheduleValidation.d.ts +0 -10
  103. package/dist/scheduler/bastionScheduleValidation.d.ts.map +0 -1
  104. package/dist/scheduler/bastionScheduleValidation.js +0 -410
  105. package/dist/scheduler/bastionScheduleValidation.js.map +0 -1
  106. package/dist/scheduler/scheduler.d.ts +0 -267
  107. package/dist/scheduler/scheduler.d.ts.map +0 -1
  108. package/dist/scheduler/scheduler.js.map +0 -1
  109. /package/dist/esm/{scheduler/bastionSchedule.js → common/boundedRows.js} +0 -0
  110. /package/dist/esm/{scheduler/scheduler.js → common/metricStats.js} +0 -0
@@ -0,0 +1,514 @@
1
+ import { isBoundedRows, isCount, isDateTime, isFiniteNumber, isRecord, isString, isStringArray } from './validationHelpers.js';
2
+ import { STORY_KEYS, STORY_LIMITS, } from './utilizationStories.js';
3
+ const PROVIDERS = new Set(['azure', 'aws']);
4
+ const METRIC_ROLES = new Set(['primary', 'secondary', 'context']);
5
+ const DIMENSION_POLICIES = new Set(['average', 'sum', 'max', 'split']);
6
+ const METRIC_VISUALS = new Set(['sparkline', 'trend', 'capacity-bar', 'mix-bar', 'event-strip']);
7
+ const BILLED_ON = new Set(['allocated', 'used', 'included']);
8
+ const AXIS_MAX_SOURCES = new Set(['config', 'capacity', 'observed']);
9
+ const SCALE_MODES = new Set(['fixed', 'autoscale', 'serverless']);
10
+ const RUN_MODES = new Set(['stop-start', 'auto-pause', 'scale-to-zero', 'always-on']);
11
+ const RUNNING_BASES = new Set(['power-state', 'billing-hours', 'metric-presence']);
12
+ const SIZING_VERDICTS = new Set(['oversized', 'undersized', 'busy', 'rebalance', 'mostly-off', 'idle', 'no-telemetry', 'fits', 'unknown']);
13
+ const SCHEDULE_FITS = new Set(['good', 'fair', 'low', 'done', 'insufficient-data', 'not-applicable']);
14
+ const SCHEDULE_FITS_REQUIRING_CORROBORATION = new Set(['good', 'fair', 'low']);
15
+ const SCHEDULE_ACTIONS = new Set(['stop', 'pause', 'scale']);
16
+ const TELEMETRY_STATUSES = new Set(['collected', 'partial', 'no-series', 'unavailable']);
17
+ const CONFIDENCES = new Set(['high', 'medium', 'low']);
18
+ const BENEFIT_TYPES = new Set(['reservation', 'savings-plan']);
19
+ const SKU_OPTION_KINDS = new Set(['same-shape', 'fits-usage', 'trade-off', 'cross-platform']);
20
+ const CAPABILITY_STATES = new Set(['enabled', 'disabled', 'partial', 'unknown', 'not-applicable']);
21
+ const RIGHT_SKU_VERDICTS = new Set(['modernise', 'downsize', 'consider', 'keep']);
22
+ const BACKUP_RUN_STATES = new Set(['ok', 'failed', null]);
23
+ const STORY_KEY_SET = new Set(STORY_KEYS);
24
+ const STORY_CELL_KINDS = new Set([
25
+ 'text',
26
+ 'number',
27
+ 'money',
28
+ 'percent',
29
+ 'mark',
30
+ 'dot',
31
+ 'sparkline',
32
+ 'dual',
33
+ 'capacity-bar',
34
+ 'mix-bar',
35
+ 'event-strip',
36
+ 'weekly-grid',
37
+ ]);
38
+ const SERIES_ROLES = new Set(['primary', 'secondary']);
39
+ const STATUS_TONES = new Set(['good', 'warn', 'bad', 'neutral', 'muted']);
40
+ const COLUMN_ROLE_CLASSES = new Set(['res', 'prim', 'sec', 'nums', 'cap', 'read', 'cost', 'save', 'dual']);
41
+ const PRODUCERS = new Set(['parser', 'strategy']);
42
+ const isNullableNumber = (value) => value === null || isFiniteNumber(value);
43
+ const isOptionalNullableNumber = (value) => value === undefined || isNullableNumber(value);
44
+ const isNullableString = (value) => value === null || isString(value);
45
+ const isText = (value) => typeof value === 'string';
46
+ const isOptionalText = (value) => value === undefined || isText(value);
47
+ const isBoolean = (value) => typeof value === 'boolean';
48
+ const isNullableSeries = (value) => Array.isArray(value) && value.every(isNullableNumber);
49
+ const isPriority = (value) => isCount(value) && value >= 1 && value <= 5;
50
+ const isStringRecord = (value) => isRecord(value) && Object.values(value).every(isText);
51
+ const isNumberRecord = (value) => isRecord(value) && Object.values(value).every(isFiniteNumber);
52
+ const inSet = (set, value) => set.has(value);
53
+ const isWeeklyGrid = (value) => Array.isArray(value) &&
54
+ value.length === STORY_LIMITS.weeklyGridDays &&
55
+ value.every(day => isNullableSeries(day) && day.length === STORY_LIMITS.weeklyGridHours);
56
+ const METRIC_STATS_FIELDS = [
57
+ 'average',
58
+ 'median',
59
+ 'p50',
60
+ 'p75',
61
+ 'p90',
62
+ 'p95',
63
+ 'p99',
64
+ 'min',
65
+ 'max',
66
+ 'frequency',
67
+ 'trend',
68
+ 'variance',
69
+ 'count',
70
+ 'totalDataPoints',
71
+ 'runningDataPoints',
72
+ 'nonRunningDataPoints',
73
+ 'runningTimePercentage',
74
+ 'longestRunningStreak',
75
+ 'longestDowntimeStreak',
76
+ 'averageUptimeStreak',
77
+ 'averageDowntimeStreak',
78
+ ];
79
+ const METRIC_STATS_OPTIONAL_FIELDS = ['uptime', 'peakHours', 'offPeakHours'];
80
+ const isMetricStats = (value) => isRecord(value) &&
81
+ METRIC_STATS_FIELDS.every(key => isFiniteNumber(value[key])) &&
82
+ METRIC_STATS_OPTIONAL_FIELDS.every(key => value[key] === undefined || isFiniteNumber(value[key]));
83
+ export const isMetricSparkline = (value, days) => {
84
+ if (!isRecord(value) || !isString(value.key) || !isString(value.metricName))
85
+ return false;
86
+ if (!inSet(METRIC_ROLES, value.role) || !inSet(METRIC_VISUALS, value.visual) || !isText(value.unit))
87
+ return false;
88
+ if (!isNullableNumber(value.axisMax) || (value.axisMaxSource !== undefined && !inSet(AXIS_MAX_SOURCES, value.axisMaxSource)))
89
+ return false;
90
+ if (value.billedOn !== undefined && !inSet(BILLED_ON, value.billedOn))
91
+ return false;
92
+ if (value.dimensions !== undefined &&
93
+ (!isRecord(value.dimensions) ||
94
+ !inSet(DIMENSION_POLICIES, value.dimensions.policy) ||
95
+ !isOptionalText(value.dimensions.name) ||
96
+ !isOptionalText(value.dimensions.value) ||
97
+ !isCount(value.dimensions.seriesCount)))
98
+ return false;
99
+ if (!isDateTime(value.start) || value.bucket !== 'P1D')
100
+ return false;
101
+ if (!isNullableSeries(value.avg) || !isNullableSeries(value.p95) || !isNullableSeries(value.max))
102
+ return false;
103
+ if (value.avg.length !== value.p95.length || value.avg.length !== value.max.length)
104
+ return false;
105
+ if (days !== undefined && value.avg.length !== days)
106
+ return false;
107
+ return isMetricStats(value.stats) && isCount(value.sourcePoints);
108
+ };
109
+ export const isCapacityDescriptor = (value) => isRecord(value) &&
110
+ isNullableString(value.sku) &&
111
+ isNullableString(value.tier) &&
112
+ isNullableNumber(value.units) &&
113
+ isText(value.unitName) &&
114
+ isOptionalNullableNumber(value.memoryGB) &&
115
+ inSet(SCALE_MODES, value.scaleMode) &&
116
+ isText(value.label);
117
+ const isHourShare = (value) => isRecord(value) && isNullableNumber(value.runningShare) && isNullableNumber(value.usageMean);
118
+ export const isRunningProfile = (value, days) => {
119
+ if (!isRecord(value) || !isString(value.signalMetric) || !inSet(RUNNING_BASES, value.basis))
120
+ return false;
121
+ if (!Array.isArray(value.corroboration) || !value.corroboration.every(basis => inSet(RUNNING_BASES, basis)))
122
+ return false;
123
+ if (value.corroboration.includes(value.basis))
124
+ return false;
125
+ if (!isNullableSeries(value.daily) || (days !== undefined && value.daily.length !== days) || !isNullableNumber(value.share))
126
+ return false;
127
+ if (value.weekly === undefined)
128
+ return true;
129
+ const weekly = value.weekly;
130
+ return (isRecord(weekly) &&
131
+ isString(weekly.timezone) &&
132
+ isWeeklyGrid(weekly.running) &&
133
+ isWeeklyGrid(weekly.usage) &&
134
+ isHourShare(weekly.businessHours) &&
135
+ isHourShare(weekly.offHours));
136
+ };
137
+ /** A running signal derived from metric presence alone is not evidence of stopped time; it needs an independent source. */
138
+ export const isCorroboratedRunningProfile = (value) => value.basis !== 'metric-presence' || value.corroboration.some(basis => basis !== 'metric-presence');
139
+ export const isCommitmentCoverage = (value) => isRecord(value) &&
140
+ isNullableNumber(value.coveragePercent) &&
141
+ Array.isArray(value.benefitTypes) &&
142
+ value.benefitTypes.every(type => inSet(BENEFIT_TYPES, type)) &&
143
+ Array.isArray(value.benefitNames) &&
144
+ value.benefitNames.every(isText) &&
145
+ isNullableNumber(value.coveredCost) &&
146
+ isNullableNumber(value.uncoveredCost) &&
147
+ isDateTime(value.windowStart) &&
148
+ isDateTime(value.windowEnd);
149
+ export const isEvidenceWindow = (value) => isRecord(value) &&
150
+ isDateTime(value.windowStart) &&
151
+ isDateTime(value.windowEnd) &&
152
+ isCount(value.days) &&
153
+ isCount(value.sampleCount) &&
154
+ inSet(TELEMETRY_STATUSES, value.telemetry) &&
155
+ inSet(CONFIDENCES, value.confidence);
156
+ const isVerdictReason = (value) => isRecord(value) && isString(value.rule) && isRecord(value.values) && Object.values(value.values).every(isNullableNumber);
157
+ export const isSkuOptionSummary = (value) => isRecord(value) && inSet(SKU_OPTION_KINDS, value.kind) && isString(value.label) && isNullableNumber(value.savingsPercent);
158
+ export const isSkuOption = (value) => isRecord(value) &&
159
+ inSet(SKU_OPTION_KINDS, value.kind) &&
160
+ isString(value.sku) &&
161
+ isString(value.label) &&
162
+ (value.capacity === undefined ||
163
+ (isRecord(value.capacity) && isNullableNumber(value.capacity.units) && isOptionalNullableNumber(value.capacity.memoryGB))) &&
164
+ isNullableNumber(value.monthlyCost) &&
165
+ isString(value.currency) &&
166
+ isNullableNumber(value.savingsPercent) &&
167
+ isNullableNumber(value.savingsMonthly) &&
168
+ isStringArray(value.lostCapabilities) &&
169
+ (value.confidence === undefined || inSet(CONFIDENCES, value.confidence));
170
+ export const isUtilizationProfile = (value, days) => {
171
+ if (!isRecord(value) || !inSet(PROVIDERS, value.provider) || !isString(value.family) || !isCapacityDescriptor(value.capacity))
172
+ return false;
173
+ if (!inSet(RUN_MODES, value.runMode) || !inSet(SIZING_VERDICTS, value.verdict) || !inSet(SCHEDULE_FITS, value.scheduleFit))
174
+ return false;
175
+ if (!isEvidenceWindow(value.evidenceWindow) || !isString(value.fingerprint))
176
+ return false;
177
+ const evidenceWindow = value.evidenceWindow;
178
+ const windowDays = days ?? evidenceWindow.days;
179
+ if (!Array.isArray(value.metrics) || !value.metrics.every(metric => isMetricSparkline(metric, windowDays)))
180
+ return false;
181
+ if (value.running !== undefined && !isRunningProfile(value.running, windowDays))
182
+ return false;
183
+ if (!Array.isArray(value.verdictReasons) || !value.verdictReasons.every(isVerdictReason) || !isStringArray(value.evidence))
184
+ return false;
185
+ if (value.scheduleAction !== undefined && !inSet(SCHEDULE_ACTIONS, value.scheduleAction))
186
+ return false;
187
+ if (value.coverage !== undefined && !isCommitmentCoverage(value.coverage))
188
+ return false;
189
+ if (value.ownerResourceId !== undefined && !isString(value.ownerResourceId))
190
+ return false;
191
+ const needsCorroboration = inSet(SCHEDULE_FITS_REQUIRING_CORROBORATION, value.scheduleFit) || value.verdict === 'mostly-off';
192
+ if (needsCorroboration) {
193
+ if (evidenceWindow.telemetry !== 'collected')
194
+ return false;
195
+ if (value.running === undefined || !isCorroboratedRunningProfile(value.running))
196
+ return false;
197
+ }
198
+ return true;
199
+ };
200
+ const isSignalSeries = (value) => isRecord(value) && isString(value.key) && isNullableNumber(value.p95) && isNullableSeries(value.sparkline);
201
+ export const isUtilizationSignal = (value) => {
202
+ if (!isRecord(value) || !inSet(SIZING_VERDICTS, value.verdict) || !inSet(SCHEDULE_FITS, value.scheduleFit))
203
+ return false;
204
+ if (!isSignalSeries(value.primary) || !inSet(TELEMETRY_STATUSES, value.telemetry))
205
+ return false;
206
+ const actionable = inSet(SCHEDULE_FITS_REQUIRING_CORROBORATION, value.scheduleFit) || value.verdict === 'mostly-off';
207
+ if (actionable && value.telemetry !== 'collected')
208
+ return false;
209
+ if (value.secondary !== undefined) {
210
+ if (!isSignalSeries(value.secondary))
211
+ return false;
212
+ const primaryLength = value.primary.sparkline.length;
213
+ if (value.secondary.sparkline.length !== primaryLength)
214
+ return false;
215
+ }
216
+ if (value.coverage !== undefined &&
217
+ (!isRecord(value.coverage) ||
218
+ !isNullableNumber(value.coverage.coveragePercent) ||
219
+ !Array.isArray(value.coverage.benefitTypes) ||
220
+ !value.coverage.benefitTypes.every(type => inSet(BENEFIT_TYPES, type))))
221
+ return false;
222
+ return value.betterSku === undefined || isSkuOptionSummary(value.betterSku);
223
+ };
224
+ const isCapabilityDetails = (value) => isRecord(value) && Object.values(value).every(detail => detail === null || isText(detail) || isFiniteNumber(detail) || isBoolean(detail));
225
+ export const isProtectionCapability = (value) => isRecord(value) &&
226
+ isString(value.key) &&
227
+ inSet(CAPABILITY_STATES, value.state) &&
228
+ isBoolean(value.required) &&
229
+ isText(value.label) &&
230
+ (value.details === undefined || isCapabilityDetails(value.details)) &&
231
+ isOptionalNullableNumber(value.costIfEnabled) &&
232
+ isRecord(value.evidence) &&
233
+ isString(value.evidence.source) &&
234
+ (value.evidence.observedAt === undefined || value.evidence.observedAt === null || isDateTime(value.evidence.observedAt));
235
+ export const isProtectionProfile = (value) => isRecord(value) &&
236
+ inSet(PROVIDERS, value.provider) &&
237
+ Array.isArray(value.capabilities) &&
238
+ value.capabilities.every(isProtectionCapability) &&
239
+ isStringArray(value.missingRequired) &&
240
+ isOptionalNullableNumber(value.slaPercent);
241
+ // ---- Cells
242
+ const isSparklineCell = (value) => inSet(SERIES_ROLES, value.role) &&
243
+ isNullableSeries(value.values) &&
244
+ isNullableNumber(value.axisMax) &&
245
+ isText(value.unit) &&
246
+ isNullableNumber(value.p95) &&
247
+ isText(value.label);
248
+ export const isStoryCell = (value) => {
249
+ if (!isRecord(value) || !inSet(STORY_CELL_KINDS, value.kind))
250
+ return false;
251
+ switch (value.kind) {
252
+ case 'text':
253
+ return (value.value === null || isText(value.value)) && (value.detail === undefined || value.detail === null || isText(value.detail));
254
+ case 'number':
255
+ return isNullableNumber(value.value) && isOptionalText(value.unit) && (value.decimals === undefined || isCount(value.decimals));
256
+ case 'money':
257
+ return isNullableNumber(value.value) && isString(value.currency);
258
+ case 'percent':
259
+ return isNullableNumber(value.value);
260
+ case 'mark':
261
+ return (isString(value.state) && isText(value.label) && inSet(STATUS_TONES, value.tone) && isOptionalText(value.icon) && isOptionalText(value.hint));
262
+ case 'dot':
263
+ return isBoolean(value.present) && inSet(STATUS_TONES, value.tone) && isOptionalText(value.label) && isOptionalText(value.hint);
264
+ case 'sparkline':
265
+ return isSparklineCell(value);
266
+ case 'dual':
267
+ return (isRecord(value.primary) &&
268
+ value.primary.kind === 'sparkline' &&
269
+ isSparklineCell(value.primary) &&
270
+ (value.secondary === null ||
271
+ (isRecord(value.secondary) &&
272
+ value.secondary.kind === 'sparkline' &&
273
+ isSparklineCell(value.secondary) &&
274
+ value.secondary.values.length === value.primary.values.length)));
275
+ case 'capacity-bar':
276
+ return isNullableNumber(value.used) && isNullableNumber(value.total) && isText(value.unit) && isText(value.label);
277
+ case 'mix-bar':
278
+ return (Array.isArray(value.parts) &&
279
+ value.parts.every(part => isRecord(part) && isString(part.key) && isText(part.label) && isFiniteNumber(part.value)) &&
280
+ isText(value.unit) &&
281
+ isFiniteNumber(value.total));
282
+ case 'event-strip':
283
+ return (Array.isArray(value.events) &&
284
+ value.events.every(event => inSet(BACKUP_RUN_STATES, event)) &&
285
+ isDateTime(value.start) &&
286
+ isOptionalText(value.label));
287
+ case 'weekly-grid':
288
+ return (isString(value.timezone) && isWeeklyGrid(value.running) && isNullableNumber(value.businessHoursShare) && isNullableNumber(value.offHoursShare));
289
+ default:
290
+ return false;
291
+ }
292
+ };
293
+ export const isStoryColumn = (value) => isRecord(value) &&
294
+ isString(value.key) &&
295
+ isText(value.label) &&
296
+ inSet(STORY_CELL_KINDS, value.cell) &&
297
+ isPriority(value.priority) &&
298
+ (value.roleClass === undefined || inSet(COLUMN_ROLE_CLASSES, value.roleClass)) &&
299
+ isOptionalText(value.hint);
300
+ /** Every column has a cell on the row, and the cell's kind matches the column's renderer. */
301
+ export const hasCellsForColumns = (row, columns) => columns.every(column => {
302
+ const cell = row.cells[column.key];
303
+ return cell !== undefined && cell.kind === column.cell;
304
+ });
305
+ // ---- Rows
306
+ /** Re-widen a narrowed row so story-specific fields can be inspected without index-signature casts. */
307
+ const fields = (value) => value;
308
+ const isStoryRowBase = (value) => isRecord(value) &&
309
+ ['resourceId', 'name', 'type', 'subscriptionId', 'tenantId', 'companyId', 'currency', 'fingerprint'].every(key => isString(value[key])) &&
310
+ isText(value.location) &&
311
+ isNullableNumber(value.spend30d) &&
312
+ isNullableNumber(value.savingsMax) &&
313
+ (value.ownerResourceId === undefined || isString(value.ownerResourceId)) &&
314
+ isRecord(value.cells) &&
315
+ Object.values(value.cells).every(isStoryCell);
316
+ export const isOversizedResourceRow = (value, days) => {
317
+ if (!isStoryRowBase(value))
318
+ return false;
319
+ const row = fields(value);
320
+ return isUtilizationProfile(row.profile, days) && (row.betterSku === undefined || isSkuOptionSummary(row.betterSku));
321
+ };
322
+ export const isRightSkuRow = (value) => {
323
+ if (!isStoryRowBase(value))
324
+ return false;
325
+ const row = fields(value);
326
+ return (isSkuOption(row.current) &&
327
+ Array.isArray(row.options) &&
328
+ row.options.every(isSkuOption) &&
329
+ inSet(RIGHT_SKU_VERDICTS, row.verdict) &&
330
+ isRecord(row.usage) &&
331
+ isNullableNumber(row.usage.primaryP95) &&
332
+ isNullableNumber(row.usage.secondaryP95) &&
333
+ inSet(TELEMETRY_STATUSES, row.usage.telemetry));
334
+ };
335
+ /** Schedule candidates always carry a corroborated weekly running profile. */
336
+ export const isScheduleCandidateRow = (value, days) => {
337
+ if (!isStoryRowBase(value))
338
+ return false;
339
+ const row = fields(value);
340
+ if (!isUtilizationProfile(row.profile, days) || !inSet(SCHEDULE_ACTIONS, row.action))
341
+ return false;
342
+ const profile = row.profile;
343
+ return profile.running !== undefined && profile.running.weekly !== undefined && isCorroboratedRunningProfile(profile.running);
344
+ };
345
+ export const isResilienceRow = (value) => {
346
+ if (!isStoryRowBase(value))
347
+ return false;
348
+ const row = fields(value);
349
+ return (isProtectionProfile(row.protection) &&
350
+ (row.backupRuns === undefined || (Array.isArray(row.backupRuns) && row.backupRuns.every(run => inSet(BACKUP_RUN_STATES, run)))) &&
351
+ (row.lastRecoveryPointAt === undefined || row.lastRecoveryPointAt === null || isDateTime(row.lastRecoveryPointAt)) &&
352
+ isOptionalNullableNumber(row.uptimeDays) &&
353
+ (row.activeHealthEvents === undefined || isCount(row.activeHealthEvents)));
354
+ };
355
+ export const isHybridBenefitRow = (value) => {
356
+ if (!isStoryRowBase(value))
357
+ return false;
358
+ const row = fields(value);
359
+ return (['productFamily', 'serviceModel', 'configurationStatus', 'technicalEligibilityStatus', 'coverageStatus', 'entitlementStatus'].every(key => isString(row[key])) &&
360
+ isNullableString(row.edition) &&
361
+ isNullableNumber(row.vCpuCount) &&
362
+ isRecord(row.decision) &&
363
+ isString(row.decision.status) &&
364
+ isText(row.decision.headline) &&
365
+ isNullableNumber(row.decision.paybackMonths) &&
366
+ isString(row.decision.confidence) &&
367
+ isStringArray(row.reasonCodes) &&
368
+ isNullableNumber(row.licenceObservedStableDays));
369
+ };
370
+ export const isCommitmentRow = (value) => {
371
+ if (!isStoryRowBase(value))
372
+ return false;
373
+ const row = fields(value);
374
+ return (isCommitmentCoverage(row.coverage) &&
375
+ (row.utilization === undefined || (isRecord(row.utilization) && isString(row.utilization.primaryKey) && isNullableNumber(row.utilization.p95))));
376
+ };
377
+ /** Row guard for a story key; unknown keys reject every row. */
378
+ export const storyRowGuard = (storyKey, days) => {
379
+ switch (storyKey) {
380
+ case 'oversized-resources':
381
+ return (row) => isOversizedResourceRow(row, days);
382
+ case 'right-sku':
383
+ return isRightSkuRow;
384
+ case 'schedule-candidates':
385
+ return (row) => isScheduleCandidateRow(row, days);
386
+ case 'resilience-recovery':
387
+ return isResilienceRow;
388
+ case 'hybrid-benefit':
389
+ return isHybridBenefitRow;
390
+ case 'commitments':
391
+ return isCommitmentRow;
392
+ default:
393
+ return (_row) => false;
394
+ }
395
+ };
396
+ export const isStorySummary = (value) => isRecord(value) &&
397
+ isRecord(value.counts) &&
398
+ Object.values(value.counts).every(isCount) &&
399
+ isNumberRecord(value.spend) &&
400
+ isString(value.currency) &&
401
+ isOptionalText(value.note);
402
+ export const isStorySection = (value, storyKey, limit, days) => {
403
+ if (!isRecord(value) || !isString(value.resourceType) || !isString(value.family))
404
+ return false;
405
+ if (!Array.isArray(value.columns) || !value.columns.every(isStoryColumn))
406
+ return false;
407
+ if (new Set(value.columns.map(column => column.key)).size !== value.columns.length)
408
+ return false;
409
+ if (!isBoundedRows(value, limit, storyRowGuard(storyKey, days)))
410
+ return false;
411
+ const columns = value.columns;
412
+ return value.rows.every(row => hasCellsForColumns(row, columns));
413
+ };
414
+ /** Every row of an artifact belongs to the artifact's scope; a row from another company, tenant or subscription is rejected. */
415
+ export const isRowInScope = (row, scope) => row.companyId === scope.companyId && row.tenantId === scope.tenantId && row.subscriptionId === scope.subscriptionId;
416
+ const isScope = (value) => isRecord(value) && ['companyId', 'tenantId', 'subscriptionId', 'currency'].every(key => isString(value[key])) && isText(value.displayName);
417
+ export const isStoryArtifact = (value, storyKey) => {
418
+ if (!isRecord(value) || !isString(value.storyKey) || !STORY_KEY_SET.has(value.storyKey))
419
+ return false;
420
+ if (storyKey !== undefined && value.storyKey !== storyKey)
421
+ return false;
422
+ if (!isScope(value.scope))
423
+ return false;
424
+ if (!isRecord(value.generation) || !isString(value.generation.sourceRunId) || !isDateTime(value.generation.generatedAt))
425
+ return false;
426
+ if (!isRecord(value.window) ||
427
+ !isDateTime(value.window.start) ||
428
+ !isDateTime(value.window.end) ||
429
+ !isCount(value.window.days) ||
430
+ value.window.days < 1 ||
431
+ !isString(value.window.timezone))
432
+ return false;
433
+ if (!isStorySummary(value.summary))
434
+ return false;
435
+ const days = value.window.days;
436
+ const key = value.storyKey;
437
+ if (!Array.isArray(value.sections) || !value.sections.every(section => isStorySection(section, key, STORY_LIMITS.sectionRows, days)))
438
+ return false;
439
+ const scope = value.scope;
440
+ return value.sections.every(section => section.rows.every(row => isRowInScope(row, scope)));
441
+ };
442
+ /** Bounded sample embedded in the evidence pack; rows are validated for `storyKey`, window days are not known here. */
443
+ export const isStorySample = (value, storyKey) => isRecord(value) &&
444
+ isStorySummary(value.summary) &&
445
+ Array.isArray(value.sections) &&
446
+ value.sections.every(section => isStorySection(section, storyKey, STORY_LIMITS.sampleRows));
447
+ export const isStoryFingerprint = (value) => isRecord(value) &&
448
+ isString(value.storyKey) &&
449
+ STORY_KEY_SET.has(value.storyKey) &&
450
+ isString(value.resourceId) &&
451
+ isString(value.fingerprint) &&
452
+ isNullableNumber(value.savingsMax);
453
+ /** Bounded, de-duplicated (storyKey + fingerprint) fingerprint rows for one history period. */
454
+ export const isStoryFingerprintRows = (value) => isBoundedRows(value, STORY_LIMITS.historyFingerprints, isStoryFingerprint) &&
455
+ new Set(value.rows.map(row => `${row.storyKey}|${row.fingerprint}`)).size === value.rows.length;
456
+ export const isReportingStories = (value) => isRecord(value) && Object.entries(value).every(([key, sample]) => STORY_KEY_SET.has(key) && isStorySample(sample, key));
457
+ // ---- Engine config schemas
458
+ export const isUtilizationMetricEntry = (value) => isRecord(value) &&
459
+ isString(value.key) &&
460
+ isString(value.metricName) &&
461
+ isOptionalText(value.collection) &&
462
+ inSet(METRIC_ROLES, value.role) &&
463
+ (value.visual === undefined || inSet(METRIC_VISUALS, value.visual)) &&
464
+ isOptionalText(value.unit) &&
465
+ (value.axisMax === undefined || isFiniteNumber(value.axisMax)) &&
466
+ isOptionalText(value.axisMaxExpression) &&
467
+ (value.billedOn === undefined || inSet(BILLED_ON, value.billedOn)) &&
468
+ isOptionalText(value.transform) &&
469
+ isOptionalText(value.requires) &&
470
+ isOptionalText(value.when) &&
471
+ (value.dimensions === undefined ||
472
+ (isRecord(value.dimensions) && inSet(DIMENSION_POLICIES, value.dimensions.policy) && isOptionalText(value.dimensions.name))) &&
473
+ (value.priority === undefined || isPriority(value.priority));
474
+ const isMetricEntries = (value) => Array.isArray(value) && value.every(isUtilizationMetricEntry);
475
+ export const isUtilizationProfileEntry = (value) => isRecord(value) &&
476
+ isString(value.family) &&
477
+ (value.producer === undefined || inSet(PRODUCERS, value.producer)) &&
478
+ isOptionalText(value.capacityLabel) &&
479
+ inSet(RUN_MODES, value.runMode) &&
480
+ (value.runningSignal === undefined || isNullableString(value.runningSignal)) &&
481
+ (value.scheduleFit === undefined || isBoolean(value.scheduleFit) || inSet(SCHEDULE_ACTIONS, value.scheduleFit)) &&
482
+ (value.variants === undefined ||
483
+ (Array.isArray(value.variants) &&
484
+ value.variants.every(variant => isRecord(variant) &&
485
+ isString(variant.when) &&
486
+ isMetricEntries(variant.metrics) &&
487
+ (variant.verdictRules === undefined || isStringRecord(variant.verdictRules))))) &&
488
+ (value.metrics === undefined || isMetricEntries(value.metrics)) &&
489
+ (value.verdictRules === undefined || isStringRecord(value.verdictRules)) &&
490
+ (value.skuAlternatives === undefined ||
491
+ (isRecord(value.skuAlternatives) &&
492
+ isString(value.skuAlternatives.source) &&
493
+ (value.skuAlternatives.capabilityKeys === undefined || isStringArray(value.skuAlternatives.capabilityKeys))));
494
+ export const isUtilizationProfileConfig = (value) => isRecord(value) &&
495
+ value.version === 1 &&
496
+ isRecord(value.defaults) &&
497
+ isCount(value.defaults.windowDays) &&
498
+ value.defaults.windowDays >= 1 &&
499
+ value.defaults.bucket === 'P1D' &&
500
+ isBoolean(value.defaults.weeklyProfile) &&
501
+ isStringRecord(value.defaults.verdictRules) &&
502
+ isRecord(value.profiles) &&
503
+ Object.entries(value.profiles).every(([type, entry]) => type === type.toLowerCase() && isUtilizationProfileEntry(entry));
504
+ export const isResilienceProfileConfig = (value) => isRecord(value) &&
505
+ value.version === 1 &&
506
+ isRecord(value.profiles) &&
507
+ Object.entries(value.profiles).every(([type, entry]) => type === type.toLowerCase() &&
508
+ isRecord(entry) &&
509
+ Array.isArray(entry.capabilities) &&
510
+ entry.capabilities.every(capability => isRecord(capability) &&
511
+ isString(capability.key) &&
512
+ isBoolean(capability.required) &&
513
+ isString(capability.source) &&
514
+ isOptionalText(capability.sla)));
@@ -0,0 +1,18 @@
1
+ export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ export const isString = (value) => typeof value === 'string' && value.trim().length > 0;
3
+ export const isOptionalString = (value) => value === undefined || isString(value);
4
+ export const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
5
+ export const isOptionalFiniteNumber = (value) => value === undefined || isFiniteNumber(value);
6
+ export const isOptionalBoolean = (value) => value === undefined || typeof value === 'boolean';
7
+ export const isCount = (value) => isFiniteNumber(value) && Number.isInteger(value) && value >= 0;
8
+ export const isDateTime = (value) => isString(value) && Number.isFinite(Date.parse(value));
9
+ export const isStringArray = (value) => Array.isArray(value) && value.every(isString);
10
+ export const isCountRecord = (value) => isRecord(value) && Object.values(value).every(isCount);
11
+ export const countTotal = (value) => Object.values(value).reduce((total, count) => total + count, 0);
12
+ export const hasOptionalStrings = (value, keys) => keys.every(key => isOptionalString(value[key]));
13
+ export const hasOptionalNumbers = (value, keys) => keys.every(key => isOptionalFiniteNumber(value[key]));
14
+ export const isBoundedRows = (value, limit, isRow) => {
15
+ if (!isRecord(value) || !isCount(value.totalCount) || !isCount(value.omittedCount) || !Array.isArray(value.rows))
16
+ return false;
17
+ return value.rows.length <= limit && value.rows.every(isRow) && value.totalCount === value.rows.length + value.omittedCount;
18
+ };
package/dist/esm/index.js CHANGED
@@ -20,6 +20,8 @@ export * from './azure/metrics.js';
20
20
  export * from './azure/payloads.js';
21
21
  export * from './azure/portalArtifacts.js';
22
22
  export * from './azure/dataProtection.js';
23
+ export * from './azure/resilienceFacts.js';
24
+ export * from './azure/resilienceFactsValidation.js';
23
25
  export * from './azure/prices.js';
24
26
  export * from './azure/relationships.js';
25
27
  export * from './azure/recommendations.js';
@@ -32,6 +34,8 @@ export * from './azure/resources.js';
32
34
  export * from './azure/reports.js';
33
35
  export * from './azure/reportEvidence.js';
34
36
  export * from './azure/reportEvidenceValidation.js';
37
+ export * from './common/utilizationStories.js';
38
+ export * from './common/utilizationStoriesValidation.js';
35
39
  export * from './azure/reportDailySpend.js';
36
40
  export * from './azure/reportDailySpendValidation.js';
37
41
  export * from './azure/reportSpend.js';
@@ -63,9 +67,6 @@ export * from './common/artifactEvidence.js';
63
67
  export * from './company/index.js';
64
68
  export * from './customProperties/index.js';
65
69
  export * from './scheduler/index.js';
66
- export * from './scheduler/bastionSchedule.js';
67
- export * from './scheduler/bastionScheduleProfile.js';
68
- export * from './scheduler/bastionScheduleValidation.js';
69
70
  export * from './ai/index.js';
70
71
  export * from './environment/index.js';
71
72
  export * from './events/index.js';
@@ -1,3 +1,4 @@
1
- export * from './scheduler.js';
1
+ export * from './schedulerContracts.js';
2
+ export * from './schedulerValidation.js';
2
3
  export * from './resourceStrategy.js';
3
4
  export * from './weeklyAvailabilityFixtures.js';
@@ -25,7 +25,6 @@ export function isResourceSchedulingCapabilityProjection(value) {
25
25
  'automation',
26
26
  'dependencies',
27
27
  'acknowledgement',
28
- 'evidence',
29
28
  ];
30
29
  if (!hasOnlyKeys(value, allowed))
31
30
  return false;
@@ -37,7 +36,6 @@ export function isResourceSchedulingCapabilityProjection(value) {
37
36
  const restore = value.restore;
38
37
  const automation = value.automation;
39
38
  const dependencies = value.dependencies;
40
- const evidence = value.evidence;
41
39
  if (!isCapabilityRef(value.capability) ||
42
40
  !isBoundedString(value.contentHash, 200) ||
43
41
  !isIsoTimestamp(value.publishedAtUtc) ||
@@ -59,7 +57,6 @@ export function isResourceSchedulingCapabilityProjection(value) {
59
57
  !isRecord(restore) ||
60
58
  !isRecord(automation) ||
61
59
  !isRecord(dependencies) ||
62
- !isRecord(evidence) ||
63
60
  !hasOnlyKeys(executionPolicy, ['authoring', 'reduce', 'restore', 'disableReason', 'supersededBy']) ||
64
61
  !hasOnlyKeys(baseline, ['policy', 'mutationDomains', 'coupledValueLabels', 'driftPolicy']) ||
65
62
  !hasOnlyKeys(cost, [
@@ -80,8 +77,7 @@ export function isResourceSchedulingCapabilityProjection(value) {
80
77
  !hasOnlyKeys(disruption, ['reversibility', 'risk', 'affectedScopeLabel', 'expectedDowntimeMinutes', 'supportedBusyPolicies']) ||
81
78
  !hasOnlyKeys(restore, ['restoreLeadMinutes', 'capacityReturnRisk', 'retryPolicyLabel', 'escalationClass']) ||
82
79
  !hasOnlyKeys(automation, ['ownership', 'conflictingControllerLabels']) ||
83
- !hasOnlyKeys(dependencies, ['hasDependencies', 'summary', 'mutableIdentityRisk']) ||
84
- !hasOnlyKeys(evidence, ['sourceUrls', 'labResult', 'verifiedAtUtc', 'expiresAtUtc'])) {
80
+ !hasOnlyKeys(dependencies, ['hasDependencies', 'summary', 'mutableIdentityRisk'])) {
85
81
  return false;
86
82
  }
87
83
  if (!['allowed', 'blocked'].includes(String(executionPolicy.authoring)) ||
@@ -166,27 +162,15 @@ export function isResourceSchedulingCapabilityProjection(value) {
166
162
  executionPolicy.restore === 'blocked';
167
163
  if (requiresDisableReason && !isBoundedString(executionPolicy.disableReason, 500))
168
164
  return false;
165
+ if (executionPolicy.reduce === 'allowed' && executionPolicy.restore !== 'allowed')
166
+ return false;
169
167
  if (cost.billingClass === 'C-no-material-saving' && (executionPolicy.authoring !== 'blocked' || executionPolicy.reduce !== 'blocked'))
170
168
  return false;
171
169
  if (value.strategy === 'recreate') {
172
170
  if (!isRecord(value.acknowledgement) || value.acknowledgement.severity !== 'destructive')
173
171
  return false;
174
172
  }
175
- if (!isBoundedStringArray(evidence.sourceUrls, RESOURCE_STRATEGY_CONTRACT_LIMITS.evidenceReferences) ||
176
- !['not-run', 'failed', 'passed'].includes(String(evidence.labResult)) ||
177
- !(evidence.verifiedAtUtc === null || isIsoTimestamp(evidence.verifiedAtUtc)) ||
178
- !(evidence.expiresAtUtc === null || isIsoTimestamp(evidence.expiresAtUtc))) {
179
- return false;
180
- }
181
- if (evidence.labResult === 'passed' && evidence.verifiedAtUtc === null)
182
- return false;
183
- const allowsAuthoringOrReduce = executionPolicy.authoring === 'allowed' || executionPolicy.reduce === 'allowed';
184
- if (allowsAuthoringOrReduce && (evidence.labResult !== 'passed' || evidence.verifiedAtUtc === null || evidence.sourceUrls.length === 0)) {
185
- return false;
186
- }
187
- return !(typeof evidence.verifiedAtUtc === 'string' &&
188
- typeof evidence.expiresAtUtc === 'string' &&
189
- Date.parse(evidence.expiresAtUtc) < Date.parse(evidence.verifiedAtUtc));
173
+ return true;
190
174
  }
191
175
  const readinessStates = new Set([
192
176
  'ready',
@@ -9,10 +9,12 @@ export const RESOURCE_STRATEGY_CONTRACT_LIMITS = {
9
9
  grantGroups: 128,
10
10
  permissionOperations: 8192,
11
11
  listResults: 100,
12
- dryRunChecks: 8,
12
+ dryRunChecks: 7,
13
13
  dryRunReasonCodes: 32,
14
14
  dryRunDtoBytes: 16384,
15
- dryRunMaxTtlMs: 5 * 60000,
15
+ // Stable readiness evidence may remain current for 24 hours. Transient
16
+ // verdicts still publish their shorter dependency expiry (normally 5 min).
17
+ dryRunMaxTtlMs: 24 * 60 * 60000,
16
18
  parameterBytes: 32768,
17
19
  publicDtoBytes: 262144,
18
20
  };