@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.
- package/dist/azure/dataProtection.d.ts +40 -0
- package/dist/azure/dataProtection.d.ts.map +1 -1
- package/dist/azure/metrics.d.ts +2 -27
- package/dist/azure/metrics.d.ts.map +1 -1
- package/dist/azure/payloads.d.ts +7 -7
- package/dist/azure/payloads.d.ts.map +1 -1
- package/dist/azure/payloads.js +22 -23
- package/dist/azure/payloads.js.map +1 -1
- package/dist/azure/reportEvidence.d.ts +7 -5
- package/dist/azure/reportEvidence.d.ts.map +1 -1
- package/dist/azure/reportEvidence.js.map +1 -1
- package/dist/azure/reportEvidenceValidation.d.ts.map +1 -1
- package/dist/azure/reportEvidenceValidation.js +5 -1
- package/dist/azure/reportEvidenceValidation.js.map +1 -1
- package/dist/azure/reportEvidenceValidationHelpers.d.ts +3 -16
- package/dist/azure/reportEvidenceValidationHelpers.d.ts.map +1 -1
- package/dist/azure/reportEvidenceValidationHelpers.js +79 -95
- package/dist/azure/reportEvidenceValidationHelpers.js.map +1 -1
- package/dist/azure/resilienceFacts.d.ts +100 -0
- package/dist/azure/resilienceFacts.d.ts.map +1 -0
- package/dist/azure/resilienceFacts.js +20 -0
- package/dist/azure/resilienceFacts.js.map +1 -0
- package/dist/azure/resilienceFactsValidation.d.ts +4 -0
- package/dist/azure/resilienceFactsValidation.d.ts.map +1 -0
- package/dist/azure/resilienceFactsValidation.js +70 -0
- package/dist/azure/resilienceFactsValidation.js.map +1 -0
- package/dist/azure/views.d.ts +7 -0
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js.map +1 -1
- package/dist/common/boundedRows.d.ts +7 -0
- package/dist/common/boundedRows.d.ts.map +1 -0
- package/dist/{scheduler/scheduler.js → common/boundedRows.js} +1 -1
- package/dist/common/boundedRows.js.map +1 -0
- package/dist/common/metricStats.d.ts +29 -0
- package/dist/common/metricStats.d.ts.map +1 -0
- package/dist/{scheduler/bastionSchedule.js → common/metricStats.js} +1 -1
- package/dist/common/metricStats.js.map +1 -0
- package/dist/common/utilizationStories.d.ts +516 -0
- package/dist/common/utilizationStories.d.ts.map +1 -0
- package/dist/common/utilizationStories.js +21 -0
- package/dist/common/utilizationStories.js.map +1 -0
- package/dist/common/utilizationStoriesValidation.d.ts +48 -0
- package/dist/common/utilizationStoriesValidation.d.ts.map +1 -0
- package/dist/common/utilizationStoriesValidation.js +552 -0
- package/dist/common/utilizationStoriesValidation.js.map +1 -0
- package/dist/common/validationHelpers.d.ts +18 -0
- package/dist/common/validationHelpers.d.ts.map +1 -0
- package/dist/common/validationHelpers.js +36 -0
- package/dist/common/validationHelpers.js.map +1 -0
- package/dist/esm/azure/payloads.js +19 -20
- package/dist/esm/azure/reportEvidenceValidation.js +5 -1
- package/dist/esm/azure/reportEvidenceValidationHelpers.js +2 -18
- package/dist/esm/azure/resilienceFacts.js +16 -0
- package/dist/esm/azure/resilienceFactsValidation.js +64 -0
- package/dist/esm/common/utilizationStories.js +17 -0
- package/dist/esm/common/utilizationStoriesValidation.js +514 -0
- package/dist/esm/common/validationHelpers.js +18 -0
- package/dist/esm/index.js +4 -3
- package/dist/esm/scheduler/index.js +2 -1
- package/dist/esm/scheduler/resourceStrategyCapabilityValidation.js +4 -20
- package/dist/esm/scheduler/resourceStrategyContracts.js +4 -2
- package/dist/esm/scheduler/resourceStrategyScheduleValidation.js +109 -4
- package/dist/esm/scheduler/schedulerContracts.js +1 -0
- package/dist/esm/scheduler/schedulerValidation.js +363 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +2 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/resourceStrategyCapabilityValidation.d.ts.map +1 -1
- package/dist/scheduler/resourceStrategyCapabilityValidation.js +4 -20
- package/dist/scheduler/resourceStrategyCapabilityValidation.js.map +1 -1
- package/dist/scheduler/resourceStrategyContracts.d.ts +26 -9
- package/dist/scheduler/resourceStrategyContracts.d.ts.map +1 -1
- package/dist/scheduler/resourceStrategyContracts.js +4 -2
- package/dist/scheduler/resourceStrategyContracts.js.map +1 -1
- package/dist/scheduler/resourceStrategyScheduleValidation.d.ts +3 -1
- package/dist/scheduler/resourceStrategyScheduleValidation.d.ts.map +1 -1
- package/dist/scheduler/resourceStrategyScheduleValidation.js +110 -4
- package/dist/scheduler/resourceStrategyScheduleValidation.js.map +1 -1
- package/dist/scheduler/schedulerContracts.d.ts +178 -0
- package/dist/scheduler/schedulerContracts.d.ts.map +1 -0
- package/dist/scheduler/schedulerContracts.js +3 -0
- package/dist/scheduler/schedulerContracts.js.map +1 -0
- package/dist/scheduler/schedulerValidation.d.ts +14 -0
- package/dist/scheduler/schedulerValidation.d.ts.map +1 -0
- package/dist/scheduler/schedulerValidation.js +378 -0
- package/dist/scheduler/schedulerValidation.js.map +1 -0
- package/package.json +5 -3
- package/dist/esm/scheduler/bastionScheduleProfile.js +0 -206
- package/dist/esm/scheduler/bastionScheduleValidation.js +0 -399
- package/dist/scheduler/bastionSchedule.d.ts +0 -113
- package/dist/scheduler/bastionSchedule.d.ts.map +0 -1
- package/dist/scheduler/bastionSchedule.js.map +0 -1
- package/dist/scheduler/bastionScheduleProfile.d.ts +0 -24
- package/dist/scheduler/bastionScheduleProfile.d.ts.map +0 -1
- package/dist/scheduler/bastionScheduleProfile.js +0 -210
- package/dist/scheduler/bastionScheduleProfile.js.map +0 -1
- package/dist/scheduler/bastionScheduleValidation.d.ts +0 -10
- package/dist/scheduler/bastionScheduleValidation.d.ts.map +0 -1
- package/dist/scheduler/bastionScheduleValidation.js +0 -410
- package/dist/scheduler/bastionScheduleValidation.js.map +0 -1
- package/dist/scheduler/scheduler.d.ts +0 -267
- package/dist/scheduler/scheduler.d.ts.map +0 -1
- package/dist/scheduler/scheduler.js.map +0 -1
- /package/dist/esm/{scheduler/bastionSchedule.js → common/boundedRows.js} +0 -0
- /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';
|
|
@@ -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
|
-
|
|
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:
|
|
12
|
+
dryRunChecks: 7,
|
|
13
13
|
dryRunReasonCodes: 32,
|
|
14
14
|
dryRunDtoBytes: 16384,
|
|
15
|
-
|
|
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
|
};
|