@spottoai/types-package 1.0.2-dev1138.1 → 1.0.2-dev1138.2

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 (90) hide show
  1. package/dist/azure/financialAnalytics.d.ts +12 -6
  2. package/dist/azure/financialAnalytics.d.ts.map +1 -1
  3. package/dist/azure/financialAnalyticsDelivery.d.ts +70 -3
  4. package/dist/azure/financialAnalyticsDelivery.d.ts.map +1 -1
  5. package/dist/azure/financialAnalyticsDelivery.js +4 -1
  6. package/dist/azure/financialAnalyticsDelivery.js.map +1 -1
  7. package/dist/azure/financialAnalyticsDeliveryValidation.d.ts +9 -1
  8. package/dist/azure/financialAnalyticsDeliveryValidation.d.ts.map +1 -1
  9. package/dist/azure/financialAnalyticsDeliveryValidation.js +190 -10
  10. package/dist/azure/financialAnalyticsDeliveryValidation.js.map +1 -1
  11. package/dist/azure/financialAnalyticsValidation.d.ts.map +1 -1
  12. package/dist/azure/financialAnalyticsValidation.js +40 -12
  13. package/dist/azure/financialAnalyticsValidation.js.map +1 -1
  14. package/dist/azure/financialDataflow.d.ts +38 -2
  15. package/dist/azure/financialDataflow.d.ts.map +1 -1
  16. package/dist/azure/financialDataflow.js.map +1 -1
  17. package/dist/azure/financialDataflowKernel.d.ts +29 -0
  18. package/dist/azure/financialDataflowKernel.d.ts.map +1 -0
  19. package/dist/azure/financialDataflowKernel.js +117 -0
  20. package/dist/azure/financialDataflowKernel.js.map +1 -0
  21. package/dist/azure/financialDataflowValidation.d.ts +4 -1
  22. package/dist/azure/financialDataflowValidation.d.ts.map +1 -1
  23. package/dist/azure/financialDataflowValidation.js +59 -4
  24. package/dist/azure/financialDataflowValidation.js.map +1 -1
  25. package/dist/azure/financialEvidenceAssessmentValidation.d.ts.map +1 -1
  26. package/dist/azure/financialEvidenceAssessmentValidation.js +7 -0
  27. package/dist/azure/financialEvidenceAssessmentValidation.js.map +1 -1
  28. package/dist/azure/financialPolicy.d.ts +16 -3
  29. package/dist/azure/financialPolicy.d.ts.map +1 -1
  30. package/dist/azure/financialPolicyDelivery.d.ts +75 -1
  31. package/dist/azure/financialPolicyDelivery.d.ts.map +1 -1
  32. package/dist/azure/financialPolicyDelivery.js +5 -1
  33. package/dist/azure/financialPolicyDelivery.js.map +1 -1
  34. package/dist/azure/financialPolicyDeliveryValidation.d.ts +16 -2
  35. package/dist/azure/financialPolicyDeliveryValidation.d.ts.map +1 -1
  36. package/dist/azure/financialPolicyDeliveryValidation.js +238 -5
  37. package/dist/azure/financialPolicyDeliveryValidation.js.map +1 -1
  38. package/dist/azure/financialPolicyKernel.d.ts +38 -0
  39. package/dist/azure/financialPolicyKernel.d.ts.map +1 -0
  40. package/dist/azure/financialPolicyKernel.js +232 -0
  41. package/dist/azure/financialPolicyKernel.js.map +1 -0
  42. package/dist/azure/financialPolicyValidation.d.ts +1 -0
  43. package/dist/azure/financialPolicyValidation.d.ts.map +1 -1
  44. package/dist/azure/financialPolicyValidation.js +106 -16
  45. package/dist/azure/financialPolicyValidation.js.map +1 -1
  46. package/dist/azure/financialSavingsAuthority.d.ts +29 -0
  47. package/dist/azure/financialSavingsAuthority.d.ts.map +1 -1
  48. package/dist/azure/financialSavingsAuthority.js +2 -1
  49. package/dist/azure/financialSavingsAuthority.js.map +1 -1
  50. package/dist/azure/financialSavingsAuthorityValidation.d.ts.map +1 -1
  51. package/dist/azure/financialSavingsAuthorityValidation.js +3 -0
  52. package/dist/azure/financialSavingsAuthorityValidation.js.map +1 -1
  53. package/dist/azure/financialSavingsCoordinateValidation.d.ts.map +1 -1
  54. package/dist/azure/financialSavingsCoordinateValidation.js +36 -0
  55. package/dist/azure/financialSavingsCoordinateValidation.js.map +1 -1
  56. package/dist/azure/financialScope.d.ts +2 -0
  57. package/dist/azure/financialScope.d.ts.map +1 -1
  58. package/dist/azure/financialScope.js +2 -0
  59. package/dist/azure/financialScope.js.map +1 -1
  60. package/dist/azure/financialScopeBaseline.d.ts +8 -2
  61. package/dist/azure/financialScopeBaseline.d.ts.map +1 -1
  62. package/dist/azure/financialScopeBaseline.js +14 -1
  63. package/dist/azure/financialScopeBaseline.js.map +1 -1
  64. package/dist/azure/financialScopeBaselineValidation.d.ts.map +1 -1
  65. package/dist/azure/financialScopeBaselineValidation.js +15 -1
  66. package/dist/azure/financialScopeBaselineValidation.js.map +1 -1
  67. package/dist/azure/financialScopeEvidence.d.ts +1 -1
  68. package/dist/azure/financialScopeEvidence.d.ts.map +1 -1
  69. package/dist/azure/views.d.ts +14 -3
  70. package/dist/azure/views.d.ts.map +1 -1
  71. package/dist/azure/views.js.map +1 -1
  72. package/dist/esm/azure/financialAnalyticsDelivery.js +3 -0
  73. package/dist/esm/azure/financialAnalyticsDeliveryValidation.js +184 -11
  74. package/dist/esm/azure/financialAnalyticsValidation.js +40 -12
  75. package/dist/esm/azure/financialDataflowKernel.js +111 -0
  76. package/dist/esm/azure/financialDataflowValidation.js +55 -3
  77. package/dist/esm/azure/financialEvidenceAssessmentValidation.js +7 -0
  78. package/dist/esm/azure/financialPolicyDelivery.js +4 -0
  79. package/dist/esm/azure/financialPolicyDeliveryValidation.js +227 -7
  80. package/dist/esm/azure/financialPolicyKernel.js +225 -0
  81. package/dist/esm/azure/financialPolicyValidation.js +105 -16
  82. package/dist/esm/azure/financialSavingsAuthority.js +1 -0
  83. package/dist/esm/azure/financialSavingsAuthorityValidation.js +3 -0
  84. package/dist/esm/azure/financialSavingsCoordinateValidation.js +36 -0
  85. package/dist/esm/azure/financialScope.js +2 -0
  86. package/dist/esm/azure/financialScopeBaseline.js +12 -0
  87. package/dist/esm/azure/financialScopeBaselineValidation.js +15 -1
  88. package/dist/events/cost.d.ts +4 -0
  89. package/dist/events/cost.d.ts.map +1 -1
  90. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { sha256Utf8 } from '../common/sha256.js';
2
2
  import { formatExactDecimalValue, sumCanonicalDecimals } from '../common/exactDecimal.js';
3
3
  import { toCanonicalEstimateLensV1 } from './costComposition.js';
4
4
  import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
5
+ import { isCompleteFinancialBaselinePeriodV2 } from './financialScopeBaseline.js';
5
6
  import { isFinancialScopeBaselineEnvelopeV2 } from './financialScopeBaselineValidation.js';
6
7
  import { FINANCIAL_CURRENT_SPEND_COMPOSITION_CONTRACT_VERSION_V1, FINANCIAL_DATAFLOW_SCHEMA_VERSION_V1, } from './financialDataflow.js';
7
8
  import { isFinancialBaselinePeriodV2 } from './financialScopeBaselineValidation.js';
@@ -141,6 +142,14 @@ export const isFinancialDataflowIdentityV1 = (value) => typeof value === 'string
141
142
  export const isFinancialDataflowHashV1 = (value) => typeof value === 'string' && SHA256_ID.test(value);
142
143
  export const isFinancialDataflowCurrencyV1 = (value) => typeof value === 'string' && CURRENCY.test(value);
143
144
  export const isFinancialDataflowIsoInstantV1 = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
145
+ export const isFinancialDataflowJsonGzipArtifactDescriptorV1 = (value) => isFinancialDataflowRecordV1(value) &&
146
+ hasFinancialDataflowExactFieldsV1(value, ['path', 'sha256', 'byteCount', 'mediaType', 'contentEncoding']) &&
147
+ isFinancialDataflowIdentityV1(value.path) &&
148
+ isFinancialDataflowHashV1(value.sha256) &&
149
+ Number.isSafeInteger(value.byteCount) &&
150
+ Number(value.byteCount) > 0 &&
151
+ value.mediaType === 'application/json' &&
152
+ value.contentEncoding === 'gzip';
144
153
  export const isFinancialDataflowCalendarDateV1 = (value) => {
145
154
  if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
146
155
  return false;
@@ -310,6 +319,46 @@ export const isFinancialDataflowScopeV1 = (value) => isFinancialDataflowRecordV1
310
319
  SCOPE_KINDS.has(value.kind) &&
311
320
  isFinancialDataflowIdentityV1(value.scopeId) &&
312
321
  isFinancialDataflowHashV1(value.scopeFingerprint);
322
+ export const isFinancialDataflowPeriodV1 = (value) => isFinancialDataflowRecordV1(value) &&
323
+ hasFinancialDataflowExactFieldsV1(value, ['windowKind', 'requested'], ['providerBillingPeriodId']) &&
324
+ isFinancialBaselinePeriodV2({ ...value, coverage: [], gaps: [] });
325
+ export const isFinancialDataflowScopeSelectorV1 = (value) => {
326
+ if (!isFinancialDataflowRecordV1(value) || typeof value.kind !== 'string' || !SCOPE_KINDS.has(value.kind))
327
+ return false;
328
+ if (value.kind === 'resource') {
329
+ return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'resourceIds']) &&
330
+ isFinancialDataflowSortedUniqueStringsV1(value.resourceIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers) &&
331
+ value.resourceIds.length === 1);
332
+ }
333
+ if (value.kind === 'resource-group') {
334
+ return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'resourceGroupIds']) &&
335
+ isFinancialDataflowSortedUniqueStringsV1(value.resourceGroupIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumMembers) &&
336
+ value.resourceGroupIds.length === 1);
337
+ }
338
+ if (value.kind === 'subscription' || value.kind === 'multi-subscription') {
339
+ return (hasFinancialDataflowExactFieldsV1(value, ['kind', 'subscriptionIds']) &&
340
+ isFinancialDataflowSortedUniqueStringsV1(value.subscriptionIds, FINANCIAL_DATAFLOW_LIMITS_V1.maximumProviderAccounts) &&
341
+ value.subscriptionIds.length > 0 &&
342
+ (value.kind !== 'subscription' || value.subscriptionIds.length === 1));
343
+ }
344
+ if (!hasFinancialDataflowExactFieldsV1(value, ['kind', 'tags', 'tagMatch']) ||
345
+ (value.tagMatch !== 'any' && value.tagMatch !== 'all') ||
346
+ !Array.isArray(value.tags) ||
347
+ value.tags.length === 0 ||
348
+ value.tags.length > 256) {
349
+ return false;
350
+ }
351
+ const keys = value.tags.map(tag => {
352
+ if (!isFinancialDataflowRecordV1(tag) ||
353
+ !hasFinancialDataflowExactFieldsV1(tag, ['key', 'value']) ||
354
+ !isFinancialDataflowIdentityV1(tag.key) ||
355
+ !isFinancialDataflowIdentityV1(tag.value)) {
356
+ return undefined;
357
+ }
358
+ return `${tag.key}\u0000${tag.value}`;
359
+ });
360
+ return keys.every((key) => key !== undefined) && keys.every((key, index) => index === 0 || keys[index - 1] < key);
361
+ };
313
362
  export const isFinancialDataflowCoordinateV1 = (value) => {
314
363
  if (!isFinancialDataflowRecordV1(value) ||
315
364
  !hasFinancialDataflowExactFieldsV1(value, ['companyId', 'provider', 'providerAccountRefs', 'scope', 'periodRole', 'period', 'costBasis', 'estimateLens', 'accountingCurrency'], ['requestedCurrencyCode']) ||
@@ -320,7 +369,7 @@ export const isFinancialDataflowCoordinateV1 = (value) => {
320
369
  !isFinancialDataflowScopeV1(value.scope) ||
321
370
  typeof value.periodRole !== 'string' ||
322
371
  !PERIOD_ROLES.has(value.periodRole) ||
323
- !isFinancialBaselinePeriodV2(value.period) ||
372
+ !isFinancialDataflowPeriodV1(value.period) ||
324
373
  typeof value.costBasis !== 'string' ||
325
374
  !COST_BASES.has(value.costBasis) ||
326
375
  typeof value.estimateLens !== 'string' ||
@@ -411,7 +460,7 @@ const isCompositionIdentity = (value) => {
411
460
  value.amount.currencyCode === currency &&
412
461
  isReasonCodes(value.amount.reasonCodes) &&
413
462
  value.members.some(member => member.status === 'included') &&
414
- value.members.some(member => member.status === 'unavailable'));
463
+ (value.members.some(member => member.status === 'unavailable') || value.amount.reasonCodes.includes('coverage-incomplete')));
415
464
  }
416
465
  const unresolvedCurrency = value.coordinate.accountingCurrency;
417
466
  return (value.amount.status === 'unavailable' &&
@@ -491,6 +540,9 @@ export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) =>
491
540
  return false;
492
541
  }
493
542
  includedAmounts.push(baseline.total.amount);
543
+ if (baseline.status === 'available' && !isCompleteFinancialBaselinePeriodV2(baseline.period)) {
544
+ expectedReasonCodes.add('coverage-incomplete');
545
+ }
494
546
  continue;
495
547
  }
496
548
  if (baseline.status !== 'unavailable' || member.reasonCode !== baseline.unavailableReason)
@@ -514,7 +566,7 @@ export const isCurrentSpendCompositionCompatibleV1 = (composition, baselines) =>
514
566
  catch {
515
567
  return false;
516
568
  }
517
- if (includedAmounts.length === composition.members.length) {
569
+ if (includedAmounts.length === composition.members.length && expectedReasonCodes.size === 0) {
518
570
  return composition.amount.status === 'available' && composition.amount.amount === includedTotal;
519
571
  }
520
572
  return (composition.amount.status === 'partial' &&
@@ -12,6 +12,13 @@ const EVIDENCE_ROLES = new Set([
12
12
  'retail-rate',
13
13
  'commitment-quote',
14
14
  'fx-conversion',
15
+ 'commercial-arrangement',
16
+ 'provider-access-observation',
17
+ 'configuration-timeline',
18
+ 'provider-price-schedule',
19
+ 'pricing-function',
20
+ 'charge-inclusion-policy',
21
+ 'settlement-revision',
15
22
  'eligibility-rule',
16
23
  'recommendation-scenario-set',
17
24
  'recommendation-lifecycle',
@@ -1,2 +1,6 @@
1
1
  export const FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1 = 'financial-policy-read-projection/v1';
2
2
  export const FINANCIAL_POLICY_ACTION_ATTEMPT_CONTRACT_VERSION_V1 = 'financial-policy-action-attempt/v1';
3
+ export const FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1 = 'financial-policy-definition-command/v1';
4
+ export const FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1 = 'financial-policy-read-current-pointer/v1';
5
+ export const FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1 = 'financial-policy-action-completion/v1';
6
+ export const FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1 = 'financial-policy-read-manifest/v1';
@@ -1,12 +1,52 @@
1
1
  import { sha256Utf8 } from '../common/sha256.js';
2
2
  import { parseCanonicalDecimal } from '../common/exactDecimal.js';
3
3
  import { isCanonicalExactMoney } from './financialValidationPrimitives.js';
4
- import { FINANCIAL_POLICY_ACTION_ATTEMPT_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1, } from './financialPolicyDelivery.js';
5
- import { canonicalizeFinancialDataflowJsonV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowIsoInstantV1, isFinancialDataflowRecordV1, } from './financialDataflowValidation.js';
6
- import { createFinancialPolicyEvaluationActionAuditIdV1, createFinancialPolicyEvaluationReadProjectionIdV1, isFinancialPolicyDefinitionRevisionV1, isFinancialPolicyEvaluationV1, } from './financialPolicyValidation.js';
4
+ import { isFinancialAnalyticsProjectionV1 } from './financialAnalyticsValidation.js';
5
+ import { FINANCIAL_POLICY_ACTION_ATTEMPT_CONTRACT_VERSION_V1, FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1, FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1, FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1, } from './financialPolicyDelivery.js';
6
+ import { canonicalizeFinancialDataflowJsonV1, hasFinancialDataflowExactFieldsV1, isFinancialDataflowHashV1, isFinancialDataflowIdentityV1, isFinancialDataflowIsoInstantV1, isFinancialDataflowJsonGzipArtifactDescriptorV1, isFinancialDataflowRecordV1, } from './financialDataflowValidation.js';
7
+ import { createFinancialPolicyDefinitionRevisionIdV1, createFinancialPolicyEvaluationActionAuditIdV1, createFinancialPolicyEvaluationReadProjectionIdV1, isFinancialPolicyDefinitionRevisionIdentityPreimageV1, isFinancialPolicyDefinitionRevisionV1, isFinancialPolicyEvaluationV1, } from './financialPolicyValidation.js';
7
8
  const SIGNAL_KINDS = new Set(['budget-current-spend', 'budget-forecast', 'cost-anomaly']);
8
9
  const RESULTS = new Set(['matched', 'not-matched', 'partial', 'unavailable']);
9
10
  const ACTION_STATUSES = new Set(['succeeded', 'retryable-failure', 'terminal-failure']);
11
+ const isCanonicalPolicyRevision = (value) => {
12
+ if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value))
13
+ return false;
14
+ const parsed = Number(value);
15
+ return Number.isSafeInteger(parsed) && String(parsed) === value;
16
+ };
17
+ export const createFinancialPolicyDestinationsDigestV1 = (destinationRefIds) => `sha256:${sha256Utf8(canonicalizeFinancialDataflowJsonV1([...destinationRefIds].sort()))}`;
18
+ export const isFinancialPolicyDefinitionCommandV1 = (value) => {
19
+ if (!isFinancialDataflowRecordV1(value) ||
20
+ !hasFinancialDataflowExactFieldsV1(value, ['schemaVersion', 'contractVersion', 'displayName', 'effectiveState', 'coordinateRequest', 'criteria', 'schedule', 'destinationRefIds'], ['expectedRevision']) ||
21
+ value.schemaVersion !== 1 ||
22
+ value.contractVersion !== FINANCIAL_POLICY_DEFINITION_COMMAND_CONTRACT_VERSION_V1 ||
23
+ (value.expectedRevision !== undefined && !isCanonicalPolicyRevision(value.expectedRevision)) ||
24
+ !isFinancialDataflowIdentityV1(value.displayName) ||
25
+ (value.effectiveState !== 'enabled' && value.effectiveState !== 'disabled') ||
26
+ !Array.isArray(value.destinationRefIds)) {
27
+ return false;
28
+ }
29
+ const identity = {
30
+ schemaVersion: 1,
31
+ contractVersion: 'financial-policy-definition/v1',
32
+ companyId: 'contract-company',
33
+ definitionId: 'contract-definition',
34
+ displayName: value.displayName,
35
+ revision: '1',
36
+ effectiveState: value.effectiveState,
37
+ coordinateRequest: value.coordinateRequest,
38
+ criteria: value.criteria,
39
+ schedule: value.schedule,
40
+ destinationRefIds: value.destinationRefIds,
41
+ destinationsDigest: createFinancialPolicyDestinationsDigestV1(value.destinationRefIds),
42
+ authoredAt: '2026-01-01T00:00:00.000Z',
43
+ authoredByUserId: 'contract-author',
44
+ };
45
+ if (!isFinancialPolicyDefinitionRevisionIdentityPreimageV1(identity))
46
+ return false;
47
+ const definition = { ...identity, policyDefinitionRevisionId: createFinancialPolicyDefinitionRevisionIdV1(identity) };
48
+ return isFinancialPolicyDefinitionRevisionV1(definition);
49
+ };
10
50
  const isNonNegativeDecimal = (value) => {
11
51
  if (typeof value !== 'string' || !isCanonicalExactMoney({ amount: value, currencyCode: 'AUD' }))
12
52
  return false;
@@ -33,6 +73,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
33
73
  'companyId',
34
74
  'definitionId',
35
75
  'definitionRevision',
76
+ 'policyDefinitionRevisionId',
36
77
  'coordinateId',
37
78
  'currentSpendCompositionId',
38
79
  'signalKind',
@@ -40,7 +81,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
40
81
  'result',
41
82
  'reasonCode',
42
83
  'matchedThresholds',
43
- ], ['analyticsProjectionId']) &&
84
+ ], ['analyticsProjectionId', 'analyticsProjection']) &&
44
85
  value.schemaVersion === 1 &&
45
86
  value.contractVersion === FINANCIAL_POLICY_READ_PROJECTION_CONTRACT_VERSION_V1 &&
46
87
  isFinancialDataflowHashV1(value.evaluationId) &&
@@ -48,12 +89,24 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
48
89
  isFinancialDataflowIdentityV1(value.companyId) &&
49
90
  isFinancialDataflowIdentityV1(value.definitionId) &&
50
91
  isFinancialDataflowIdentityV1(value.definitionRevision) &&
92
+ isFinancialDataflowHashV1(value.policyDefinitionRevisionId) &&
51
93
  isFinancialDataflowHashV1(value.coordinateId) &&
52
94
  isFinancialDataflowHashV1(value.currentSpendCompositionId) &&
53
95
  (value.analyticsProjectionId === undefined || isFinancialDataflowHashV1(value.analyticsProjectionId)) &&
96
+ (value.analyticsProjection === undefined || isFinancialAnalyticsProjectionV1(value.analyticsProjection)) &&
97
+ (value.analyticsProjectionId === undefined
98
+ ? value.analyticsProjection === undefined
99
+ : value.analyticsProjection?.analyticsProjectionId === value.analyticsProjectionId &&
100
+ value.analyticsProjection.currentSpendCompositionId === value.currentSpendCompositionId &&
101
+ value.analyticsProjection.coordinate.companyId === value.companyId &&
102
+ value.analyticsProjection.coordinate.periodRole === 'projection-target') &&
54
103
  typeof value.signalKind === 'string' &&
55
104
  SIGNAL_KINDS.has(value.signalKind) &&
56
- (value.signalKind === 'budget-current-spend' ? value.analyticsProjectionId === undefined : value.analyticsProjectionId !== undefined) &&
105
+ (value.signalKind === 'budget-current-spend'
106
+ ? value.analyticsProjectionId === undefined
107
+ : value.result === 'unavailable'
108
+ ? value.analyticsProjectionId === undefined || isFinancialDataflowHashV1(value.analyticsProjectionId)
109
+ : value.analyticsProjectionId !== undefined) &&
57
110
  isFinancialDataflowIsoInstantV1(value.evaluatedAt) &&
58
111
  typeof value.result === 'string' &&
59
112
  RESULTS.has(value.result) &&
@@ -61,7 +114,7 @@ export const isFinancialPolicyEvaluationReadProjectionV1 = (value) => isFinancia
61
114
  isMatchedThresholds(value.matchedThresholds) &&
62
115
  Array.isArray(value.matchedThresholds) &&
63
116
  (value.result === 'matched' ? value.matchedThresholds.length > 0 : value.matchedThresholds.length === 0);
64
- export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection, evaluation) => {
117
+ export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection, evaluation, analyticsProjection) => {
65
118
  if (!isFinancialPolicyEvaluationReadProjectionV1(projection) || !isFinancialPolicyEvaluationV1(evaluation))
66
119
  return false;
67
120
  const expected = {
@@ -72,17 +125,138 @@ export const isFinancialPolicyEvaluationReadProjectionCompatibleV1 = (projection
72
125
  companyId: evaluation.companyId,
73
126
  definitionId: evaluation.definitionId,
74
127
  definitionRevision: evaluation.definitionRevision,
128
+ policyDefinitionRevisionId: evaluation.policyDefinitionRevisionId,
75
129
  coordinateId: evaluation.coordinateId,
76
130
  currentSpendCompositionId: evaluation.currentSpendCompositionId,
77
131
  ...(evaluation.analyticsProjectionId === undefined ? {} : { analyticsProjectionId: evaluation.analyticsProjectionId }),
132
+ ...(analyticsProjection === undefined ? {} : { analyticsProjection }),
78
133
  signalKind: evaluation.signalKind,
79
134
  evaluatedAt: evaluation.evaluatedAt,
80
135
  result: evaluation.result,
81
136
  reasonCode: evaluation.reasonCode,
82
137
  matchedThresholds: evaluation.matchedThresholds,
83
138
  };
84
- return canonicalizeFinancialDataflowJsonV1(projection) === canonicalizeFinancialDataflowJsonV1(expected);
139
+ return ((evaluation.analyticsProjectionId === undefined
140
+ ? analyticsProjection === undefined
141
+ : analyticsProjection?.analyticsProjectionId === evaluation.analyticsProjectionId) &&
142
+ canonicalizeFinancialDataflowJsonV1(projection) === canonicalizeFinancialDataflowJsonV1(expected));
143
+ };
144
+ export const canonicalizeFinancialPolicyEvaluationReadCurrentPointerIdentityV1 = (value) => {
145
+ if (!isFinancialDataflowRecordV1(value) ||
146
+ !hasFinancialDataflowExactFieldsV1(value, [
147
+ 'schemaVersion',
148
+ 'contractVersion',
149
+ 'companyId',
150
+ 'definitionId',
151
+ 'signalKind',
152
+ 'pointerRevision',
153
+ 'definitionRevision',
154
+ 'policyDefinitionRevisionId',
155
+ 'evaluationId',
156
+ 'readProjectionId',
157
+ 'projectionArtifactDigest',
158
+ 'evaluatedAt',
159
+ 'promotedAt',
160
+ ]) ||
161
+ value.schemaVersion !== 1 ||
162
+ value.contractVersion !== FINANCIAL_POLICY_READ_CURRENT_POINTER_CONTRACT_VERSION_V1 ||
163
+ !isFinancialDataflowIdentityV1(value.companyId) ||
164
+ !isFinancialDataflowIdentityV1(value.definitionId) ||
165
+ typeof value.signalKind !== 'string' ||
166
+ !SIGNAL_KINDS.has(value.signalKind) ||
167
+ !Number.isSafeInteger(value.pointerRevision) ||
168
+ value.pointerRevision < 1 ||
169
+ !isFinancialDataflowIdentityV1(value.definitionRevision) ||
170
+ !isFinancialDataflowHashV1(value.policyDefinitionRevisionId) ||
171
+ !isFinancialDataflowHashV1(value.evaluationId) ||
172
+ value.readProjectionId !== createFinancialPolicyEvaluationReadProjectionIdV1(value.evaluationId) ||
173
+ !isFinancialDataflowHashV1(value.projectionArtifactDigest) ||
174
+ !isFinancialDataflowIsoInstantV1(value.evaluatedAt) ||
175
+ !isFinancialDataflowIsoInstantV1(value.promotedAt)) {
176
+ throw new TypeError('Invalid FinancialPolicyEvaluationReadCurrentPointerIdentityPreimageV1.');
177
+ }
178
+ return canonicalizeFinancialDataflowJsonV1(value);
179
+ };
180
+ export const createFinancialPolicyEvaluationReadCurrentPointerDigestV1 = (value) => `sha256:${sha256Utf8(canonicalizeFinancialPolicyEvaluationReadCurrentPointerIdentityV1(value))}`;
181
+ export const isFinancialPolicyEvaluationReadCurrentPointerV1 = (value) => {
182
+ if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'pointerDigest'))
183
+ return false;
184
+ const { pointerDigest, ...identity } = value;
185
+ try {
186
+ return (isFinancialDataflowHashV1(pointerDigest) &&
187
+ pointerDigest === createFinancialPolicyEvaluationReadCurrentPointerDigestV1(identity));
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ };
193
+ export const isFinancialPolicyEvaluationReadCurrentPointerCompatibleV1 = (pointer, projection, projectionArtifactDigest) => isFinancialPolicyEvaluationReadCurrentPointerV1(pointer) &&
194
+ isFinancialPolicyEvaluationReadProjectionV1(projection) &&
195
+ isFinancialDataflowHashV1(projectionArtifactDigest) &&
196
+ pointer.companyId === projection.companyId &&
197
+ pointer.definitionId === projection.definitionId &&
198
+ pointer.signalKind === projection.signalKind &&
199
+ pointer.definitionRevision === projection.definitionRevision &&
200
+ pointer.policyDefinitionRevisionId === projection.policyDefinitionRevisionId &&
201
+ pointer.evaluationId === projection.evaluationId &&
202
+ pointer.readProjectionId === projection.readProjectionId &&
203
+ pointer.projectionArtifactDigest === projectionArtifactDigest &&
204
+ pointer.evaluatedAt === projection.evaluatedAt;
205
+ const isReadManifestIdentity = (value) => isFinancialDataflowRecordV1(value) &&
206
+ hasFinancialDataflowExactFieldsV1(value, [
207
+ 'schemaVersion',
208
+ 'contractVersion',
209
+ 'companyId',
210
+ 'definitionId',
211
+ 'policyDefinitionRevisionId',
212
+ 'definitionRevision',
213
+ 'evaluationId',
214
+ 'readProjectionId',
215
+ 'signalKind',
216
+ 'projection',
217
+ 'publishedAt',
218
+ ]) &&
219
+ value.schemaVersion === 1 &&
220
+ value.contractVersion === FINANCIAL_POLICY_READ_MANIFEST_CONTRACT_VERSION_V1 &&
221
+ isFinancialDataflowIdentityV1(value.companyId) &&
222
+ isFinancialDataflowIdentityV1(value.definitionId) &&
223
+ isFinancialDataflowHashV1(value.policyDefinitionRevisionId) &&
224
+ isFinancialDataflowIdentityV1(value.definitionRevision) &&
225
+ isFinancialDataflowHashV1(value.evaluationId) &&
226
+ value.readProjectionId === createFinancialPolicyEvaluationReadProjectionIdV1(value.evaluationId) &&
227
+ typeof value.signalKind === 'string' &&
228
+ SIGNAL_KINDS.has(value.signalKind) &&
229
+ isFinancialDataflowJsonGzipArtifactDescriptorV1(value.projection) &&
230
+ isFinancialDataflowIsoInstantV1(value.publishedAt);
231
+ export const createFinancialPolicyEvaluationReadManifestDigestV1 = (value) => {
232
+ if (!isReadManifestIdentity(value))
233
+ throw new TypeError('Invalid FinancialPolicyEvaluationReadManifestIdentityPreimageV1.');
234
+ return `sha256:${sha256Utf8(canonicalizeFinancialDataflowJsonV1(value))}`;
235
+ };
236
+ export const isFinancialPolicyEvaluationReadManifestV1 = (value) => {
237
+ if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'manifestDigest'))
238
+ return false;
239
+ const { manifestDigest, ...identity } = value;
240
+ try {
241
+ return (isFinancialDataflowHashV1(manifestDigest) &&
242
+ manifestDigest === createFinancialPolicyEvaluationReadManifestDigestV1(identity));
243
+ }
244
+ catch {
245
+ return false;
246
+ }
85
247
  };
248
+ export const isFinancialPolicyEvaluationReadManifestCompatibleV1 = (manifest, projection, verifiedProjectionArtifactDigest) => isFinancialPolicyEvaluationReadManifestV1(manifest) &&
249
+ isFinancialPolicyEvaluationReadProjectionV1(projection) &&
250
+ isFinancialDataflowHashV1(verifiedProjectionArtifactDigest) &&
251
+ manifest.companyId === projection.companyId &&
252
+ manifest.definitionId === projection.definitionId &&
253
+ manifest.policyDefinitionRevisionId === projection.policyDefinitionRevisionId &&
254
+ manifest.definitionRevision === projection.definitionRevision &&
255
+ manifest.evaluationId === projection.evaluationId &&
256
+ manifest.readProjectionId === projection.readProjectionId &&
257
+ manifest.signalKind === projection.signalKind &&
258
+ manifest.publishedAt === projection.evaluatedAt &&
259
+ manifest.projection.sha256 === verifiedProjectionArtifactDigest;
86
260
  const isActionAttemptIdentity = (value) => isFinancialDataflowRecordV1(value) &&
87
261
  hasFinancialDataflowExactFieldsV1(value, [
88
262
  'schemaVersion',
@@ -135,3 +309,49 @@ export const isFinancialPolicyActionAttemptCompatibleV1 = (attempt, evaluation,
135
309
  Date.parse(attempt.attemptedAt) >= Date.parse(evaluation.evaluatedAt) &&
136
310
  evaluation.policyDefinitionRevisionId === definition.policyDefinitionRevisionId &&
137
311
  definition.destinationRefIds.includes(attempt.destinationRefId);
312
+ export const canonicalizeFinancialPolicyActionCompletionIdentityV1 = (value) => {
313
+ if (!isFinancialDataflowRecordV1(value) ||
314
+ !hasFinancialDataflowExactFieldsV1(value, [
315
+ 'schemaVersion',
316
+ 'contractVersion',
317
+ 'actionAuditId',
318
+ 'evaluationId',
319
+ 'companyId',
320
+ 'destinationRefId',
321
+ 'completedAt',
322
+ 'executorVersion',
323
+ ]) ||
324
+ value.schemaVersion !== 1 ||
325
+ value.contractVersion !== FINANCIAL_POLICY_ACTION_COMPLETION_CONTRACT_VERSION_V1 ||
326
+ !isFinancialDataflowHashV1(value.actionAuditId) ||
327
+ !isFinancialDataflowHashV1(value.evaluationId) ||
328
+ !isFinancialDataflowIdentityV1(value.companyId) ||
329
+ !isFinancialDataflowIdentityV1(value.destinationRefId) ||
330
+ !isFinancialDataflowIsoInstantV1(value.completedAt) ||
331
+ !isFinancialDataflowIdentityV1(value.executorVersion)) {
332
+ throw new TypeError('Invalid FinancialPolicyActionCompletionIdentityPreimageV1.');
333
+ }
334
+ return canonicalizeFinancialDataflowJsonV1(value);
335
+ };
336
+ export const createFinancialPolicyActionCompletionIdV1 = (value) => `sha256:${sha256Utf8(canonicalizeFinancialPolicyActionCompletionIdentityV1(value))}`;
337
+ export const isFinancialPolicyActionCompletionV1 = (value) => {
338
+ if (!isFinancialDataflowRecordV1(value) || !Object.prototype.hasOwnProperty.call(value, 'completionId'))
339
+ return false;
340
+ const { completionId, ...identity } = value;
341
+ try {
342
+ return (isFinancialDataflowHashV1(completionId) &&
343
+ completionId === createFinancialPolicyActionCompletionIdV1(identity));
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ };
349
+ export const isFinancialPolicyActionCompletionCompatibleV1 = (completion, evaluation, definition) => isFinancialPolicyActionCompletionV1(completion) &&
350
+ isFinancialPolicyEvaluationV1(evaluation) &&
351
+ isFinancialPolicyDefinitionRevisionV1(definition) &&
352
+ completion.actionAuditId === evaluation.actionAuditId &&
353
+ completion.evaluationId === evaluation.evaluationId &&
354
+ completion.companyId === evaluation.companyId &&
355
+ completion.completedAt === evaluation.evaluatedAt &&
356
+ definition.companyId === completion.companyId &&
357
+ definition.destinationRefIds.includes(completion.destinationRefId);
@@ -0,0 +1,225 @@
1
+ import { formatExactDecimalValue, multiplyExactDecimalValues, parseCanonicalDecimal, subtractExactDecimalValues, } from '../common/exactDecimal.js';
2
+ import { createFinancialDataflowCoordinateIdV1 } from './financialDataflowValidation.js';
3
+ import { FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1, } from './financialPolicy.js';
4
+ import { createFinancialPolicyEvaluationActionAuditIdV1, createFinancialPolicyEvaluationIdV1, createFinancialPolicyEvaluationReadProjectionIdV1, isFinancialPolicyEvaluationCompatibleV1, } from './financialPolicyValidation.js';
5
+ export const FINANCIAL_POLICY_ALGORITHM_VERSION_V1 = 'financial-policy/shared-v1';
6
+ const compare = (left, right) => {
7
+ const difference = subtractExactDecimalValues(parseCanonicalDecimal(left), parseCanonicalDecimal(right)).coefficient;
8
+ return difference < 0n ? -1 : difference > 0n ? 1 : 0;
9
+ };
10
+ const percentageMatched = (amount, denominator, threshold) => {
11
+ const parsedDenominator = parseCanonicalDecimal(denominator);
12
+ if (parsedDenominator.coefficient <= 0n)
13
+ return false;
14
+ const left = multiplyExactDecimalValues(parseCanonicalDecimal(amount), parseCanonicalDecimal('100'));
15
+ const right = multiplyExactDecimalValues(parsedDenominator, parseCanonicalDecimal(threshold));
16
+ return subtractExactDecimalValues(left, right).coefficient >= 0n;
17
+ };
18
+ /**
19
+ * Shared exact-decimal display classification for Budget surfaces. This is a
20
+ * read-model policy only; alert matching continues to use the thresholds in a
21
+ * versioned Financial Policy Definition.
22
+ */
23
+ export const classifyFinancialBudgetPositionV1 = (request) => {
24
+ if (request.budgetAmount === undefined)
25
+ return { status: 'unbudgeted' };
26
+ if (request.amount === undefined)
27
+ return { status: 'unavailable' };
28
+ try {
29
+ const atRiskPercent = request.atRiskPercent ?? '95';
30
+ const amount = parseCanonicalDecimal(request.amount);
31
+ const budget = parseCanonicalDecimal(request.budgetAmount);
32
+ const risk = parseCanonicalDecimal(atRiskPercent);
33
+ if (budget.coefficient <= 0n || risk.coefficient < 0n || compare(atRiskPercent, '100') > 0) {
34
+ return { status: 'unbudgeted' };
35
+ }
36
+ const varianceAmount = formatExactDecimalValue(subtractExactDecimalValues(amount, budget));
37
+ const status = compare(request.amount, request.budgetAmount) > 0
38
+ ? 'over-budget'
39
+ : percentageMatched(request.amount, request.budgetAmount, atRiskPercent)
40
+ ? 'at-risk'
41
+ : 'on-track';
42
+ return {
43
+ status,
44
+ varianceAmount,
45
+ amount: request.amount,
46
+ budgetAmount: request.budgetAmount,
47
+ atRiskPercent,
48
+ };
49
+ }
50
+ catch {
51
+ return { status: 'unavailable' };
52
+ }
53
+ };
54
+ const isExpectedCalendarMonthTargetPartial = (composition) => composition.coordinate.period.windowKind === 'calendar-month' &&
55
+ composition.amount.status === 'partial' &&
56
+ composition.amount.reasonCodes.length === 1 &&
57
+ composition.amount.reasonCodes[0] === 'coverage-incomplete';
58
+ /**
59
+ * Returns the exact current amount that Budget semantics may compare. An open
60
+ * calendar month is intentionally partial until the period closes; every
61
+ * other partial/unavailable state remains non-comparable.
62
+ */
63
+ export const selectFinancialBudgetComparableCurrentAmountV1 = (composition) => composition.amount.status === 'available'
64
+ ? composition.amount.amount
65
+ : isExpectedCalendarMonthTargetPartial(composition)
66
+ ? composition.amount.knownAmount
67
+ : undefined;
68
+ const budgetThresholds = (amount, budget, thresholds) => [
69
+ ...thresholds.amounts
70
+ .filter(threshold => compare(amount, threshold) >= 0)
71
+ .map(configuredValue => ({ thresholdKind: 'amount', configuredValue })),
72
+ ...thresholds.percents
73
+ .filter(threshold => percentageMatched(amount, budget, threshold))
74
+ .map(configuredValue => ({ thresholdKind: 'percent', configuredValue })),
75
+ ];
76
+ const anomalyThresholds = (definition, projection) => {
77
+ if (definition.criteria.kind !== 'cost-anomaly' || projection.status === 'unavailable' || projection.result.kind !== 'anomaly')
78
+ return [];
79
+ const configured = [
80
+ ...(definition.criteria.minimumAmount === undefined
81
+ ? []
82
+ : [{ thresholdKind: 'minimum-amount', configuredValue: definition.criteria.minimumAmount }]),
83
+ ...(definition.criteria.minimumDelta === undefined
84
+ ? []
85
+ : [{ thresholdKind: 'minimum-delta', configuredValue: definition.criteria.minimumDelta }]),
86
+ ...(definition.criteria.minimumPercentChange === undefined
87
+ ? []
88
+ : [{ thresholdKind: 'minimum-percent-change', configuredValue: definition.criteria.minimumPercentChange }]),
89
+ ];
90
+ return projection.result.events.some(event => configured.every(threshold => {
91
+ if (threshold.thresholdKind === 'minimum-amount')
92
+ return compare(event.observed.amount, threshold.configuredValue) >= 0;
93
+ if (threshold.thresholdKind === 'minimum-delta')
94
+ return compare(event.delta.amount, threshold.configuredValue) >= 0;
95
+ return percentageMatched(event.delta.amount, event.expected.amount, threshold.configuredValue);
96
+ }))
97
+ ? configured
98
+ : [];
99
+ };
100
+ const materialize = (identity, definition, currentSpend, analytics) => {
101
+ const evaluationId = createFinancialPolicyEvaluationIdV1(identity);
102
+ const evaluation = {
103
+ ...identity,
104
+ evaluationId,
105
+ readProjectionId: createFinancialPolicyEvaluationReadProjectionIdV1(evaluationId),
106
+ actionAuditId: createFinancialPolicyEvaluationActionAuditIdV1(evaluationId),
107
+ };
108
+ if (!isFinancialPolicyEvaluationCompatibleV1(evaluation, definition, currentSpend, analytics)) {
109
+ throw new TypeError('Financial policy evaluation does not bind its definition and exact financial inputs.');
110
+ }
111
+ return evaluation;
112
+ };
113
+ export const evaluateFinancialPolicyV1 = (request) => {
114
+ if (request.definition.effectiveState !== 'enabled')
115
+ return [];
116
+ const common = {
117
+ schemaVersion: 1,
118
+ contractVersion: FINANCIAL_POLICY_EVALUATION_CONTRACT_VERSION_V1,
119
+ companyId: request.definition.companyId,
120
+ policyDefinitionRevisionId: request.definition.policyDefinitionRevisionId,
121
+ definitionId: request.definition.definitionId,
122
+ definitionRevision: request.definition.revision,
123
+ coordinateId: createFinancialDataflowCoordinateIdV1(request.currentSpend.coordinate),
124
+ currentSpendCompositionId: request.currentSpend.compositionId,
125
+ evaluatedAt: request.evaluatedAt,
126
+ policyAlgorithmVersion: FINANCIAL_POLICY_ALGORITHM_VERSION_V1,
127
+ };
128
+ if (request.definition.criteria.kind === 'budget') {
129
+ const evaluations = [];
130
+ const currentThresholds = request.definition.criteria.currentSpendThresholds;
131
+ if (currentThresholds) {
132
+ const expectedOpenCalendarMonth = isExpectedCalendarMonthTargetPartial(request.currentSpend);
133
+ const comparableCurrentAmount = selectFinancialBudgetComparableCurrentAmountV1(request.currentSpend);
134
+ const matched = comparableCurrentAmount === undefined
135
+ ? []
136
+ : budgetThresholds(comparableCurrentAmount, request.definition.criteria.budget.amount, currentThresholds);
137
+ const unavailable = request.currentSpend.amount.status === 'unavailable';
138
+ const partial = request.currentSpend.amount.status === 'partial' && !expectedOpenCalendarMonth;
139
+ evaluations.push(materialize({
140
+ ...common,
141
+ signalKind: 'budget-current-spend',
142
+ result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
143
+ reasonCode: unavailable
144
+ ? 'current-spend-unavailable'
145
+ : partial
146
+ ? 'current-spend-partial'
147
+ : expectedOpenCalendarMonth && matched.length > 0
148
+ ? 'current-spend-threshold-matched-open-period'
149
+ : expectedOpenCalendarMonth
150
+ ? 'current-spend-threshold-not-matched-open-period'
151
+ : matched.length > 0
152
+ ? 'current-spend-threshold-matched'
153
+ : 'current-spend-threshold-not-matched',
154
+ matchedThresholds: matched,
155
+ }, request.definition, request.currentSpend));
156
+ }
157
+ const forecastThresholds = request.definition.criteria.forecastThresholds;
158
+ if (forecastThresholds) {
159
+ const projection = request.analytics?.forecast;
160
+ if (!projection) {
161
+ evaluations.push(materialize({
162
+ ...common,
163
+ signalKind: 'budget-forecast',
164
+ result: 'unavailable',
165
+ reasonCode: 'analytics-projection-unavailable',
166
+ matchedThresholds: [],
167
+ }, request.definition, request.currentSpend));
168
+ }
169
+ else {
170
+ const unavailable = projection.status === 'unavailable' || request.currentSpend.amount.status === 'unavailable';
171
+ const partial = projection.status === 'partial' ||
172
+ (request.currentSpend.amount.status === 'partial' && !isExpectedCalendarMonthTargetPartial(request.currentSpend));
173
+ const matched = !unavailable && !partial && projection.result.kind === 'forecast'
174
+ ? budgetThresholds(projection.result.projectedTotal.amount, request.definition.criteria.budget.amount, forecastThresholds)
175
+ : [];
176
+ evaluations.push(materialize({
177
+ ...common,
178
+ analyticsProjectionId: projection.analyticsProjectionId,
179
+ signalKind: 'budget-forecast',
180
+ result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
181
+ reasonCode: unavailable
182
+ ? 'forecast-unavailable'
183
+ : partial
184
+ ? 'forecast-partial'
185
+ : matched.length > 0
186
+ ? 'forecast-threshold-matched'
187
+ : 'forecast-threshold-not-matched',
188
+ matchedThresholds: matched,
189
+ }, request.definition, request.currentSpend, projection));
190
+ }
191
+ }
192
+ return evaluations;
193
+ }
194
+ const projection = request.analytics?.anomaly;
195
+ if (!projection) {
196
+ return [
197
+ materialize({
198
+ ...common,
199
+ signalKind: 'cost-anomaly',
200
+ result: 'unavailable',
201
+ reasonCode: 'analytics-projection-unavailable',
202
+ matchedThresholds: [],
203
+ }, request.definition, request.currentSpend),
204
+ ];
205
+ }
206
+ const unavailable = projection.status === 'unavailable' || request.currentSpend.amount.status === 'unavailable';
207
+ const partial = projection.status === 'partial' || request.currentSpend.amount.status === 'partial';
208
+ const matched = unavailable || partial ? [] : anomalyThresholds(request.definition, projection);
209
+ return [
210
+ materialize({
211
+ ...common,
212
+ analyticsProjectionId: projection.analyticsProjectionId,
213
+ signalKind: 'cost-anomaly',
214
+ result: unavailable ? 'unavailable' : partial ? 'partial' : matched.length > 0 ? 'matched' : 'not-matched',
215
+ reasonCode: unavailable
216
+ ? 'anomaly-unavailable'
217
+ : partial
218
+ ? 'anomaly-partial'
219
+ : matched.length > 0
220
+ ? 'anomaly-threshold-matched'
221
+ : 'anomaly-threshold-not-matched',
222
+ matchedThresholds: matched,
223
+ }, request.definition, request.currentSpend, projection),
224
+ ];
225
+ };