@cullet/erp-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/KIT_CONTEXT.md +39 -0
  2. package/README.md +110 -0
  3. package/dist/errors/index.d.ts +2 -0
  4. package/dist/errors/index.js +3 -0
  5. package/dist/exceptions/validation-field.d.ts +2 -0
  6. package/dist/exceptions/validation-field.js +2 -0
  7. package/dist/gate-engine-registry.js +260 -0
  8. package/dist/gate-engine-registry.js.map +1 -0
  9. package/dist/gate-types.d.ts +307 -0
  10. package/dist/gate-v1-payload.schema.js +677 -0
  11. package/dist/gate-v1-payload.schema.js.map +1 -0
  12. package/dist/index.d.ts +88 -0
  13. package/dist/index.js +153 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/outcome.js +92 -0
  16. package/dist/outcome.js.map +1 -0
  17. package/dist/parse-gate-payload.d.ts +136 -0
  18. package/dist/policies/engines/index.d.ts +3 -0
  19. package/dist/policies/engines/index.js +2 -0
  20. package/dist/policies/engines/v1/gate/index.d.ts +115 -0
  21. package/dist/policies/engines/v1/gate/index.js +84 -0
  22. package/dist/policies/engines/v1/gate/index.js.map +1 -0
  23. package/dist/policies/index.d.ts +4 -0
  24. package/dist/policies/index.js +5 -0
  25. package/dist/policy-service.d.ts +541 -0
  26. package/dist/policy-service.js +992 -0
  27. package/dist/policy-service.js.map +1 -0
  28. package/dist/validation-code.js +210 -0
  29. package/dist/validation-code.js.map +1 -0
  30. package/dist/validation-error.d.ts +686 -0
  31. package/dist/validation-error.js +757 -0
  32. package/dist/validation-error.js.map +1 -0
  33. package/dist/validation-field.d.ts +12 -0
  34. package/dist/validation-field.js +23 -0
  35. package/dist/validation-field.js.map +1 -0
  36. package/meta.json +66 -0
  37. package/package.json +59 -0
  38. package/src/core/application/commands/command.ts +28 -0
  39. package/src/core/application/commands/index.ts +2 -0
  40. package/src/core/application/commands/requested-by.ts +97 -0
  41. package/src/core/application/index.ts +31 -0
  42. package/src/core/application/policy-error-mapper.ts +60 -0
  43. package/src/core/application/ports/index.ts +14 -0
  44. package/src/core/application/ports/logger.port.ts +10 -0
  45. package/src/core/application/ports/metrics.port.ts +9 -0
  46. package/src/core/application/ports/policy-port.ts +20 -0
  47. package/src/core/application/ports/repository.port.ts +7 -0
  48. package/src/core/application/ports/temporal-repository.port.ts +14 -0
  49. package/src/core/application/ports/tracer.port.ts +16 -0
  50. package/src/core/application/queries/index.ts +2 -0
  51. package/src/core/application/queries/query.ts +53 -0
  52. package/src/core/application/temporal/index.ts +11 -0
  53. package/src/core/application/temporal/temporal-context.ts +41 -0
  54. package/src/core/application/temporal/temporal-use-case.ts +41 -0
  55. package/src/core/application/use-case.ts +24 -0
  56. package/src/core/config/core-config.instance.ts +14 -0
  57. package/src/core/config/core-config.ts +87 -0
  58. package/src/core/config/index.ts +12 -0
  59. package/src/core/config/policy-reporter.ts +60 -0
  60. package/src/core/config/silent-policy-reporter.ts +5 -0
  61. package/src/core/domain/entity.ts +69 -0
  62. package/src/core/domain/rulesets/entity-ruleset.contracts.ts +16 -0
  63. package/src/core/domain/rulesets/ruleset-registry.ts +71 -0
  64. package/src/core/domain/rulesets/ruleset.contracts.ts +15 -0
  65. package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +13 -0
  66. package/src/core/domain/temporal/index.ts +18 -0
  67. package/src/core/domain/temporal/temporal-range.ts +53 -0
  68. package/src/core/domain/temporal/temporal-snapshot.ts +47 -0
  69. package/src/core/domain/temporal/transaction-time.ts +60 -0
  70. package/src/core/domain/temporal/valid-time.ts +57 -0
  71. package/src/core/domain/value-object.ts +30 -0
  72. package/src/core/errors/app-error.ts +85 -0
  73. package/src/core/errors/authentication-error.ts +171 -0
  74. package/src/core/errors/authorization-error.ts +204 -0
  75. package/src/core/errors/business-rule-violation-error.ts +28 -0
  76. package/src/core/errors/conflict-error.ts +246 -0
  77. package/src/core/errors/error-codes.ts +60 -0
  78. package/src/core/errors/idempotency-error.ts +372 -0
  79. package/src/core/errors/index.ts +99 -0
  80. package/src/core/errors/integration-error.ts +157 -0
  81. package/src/core/errors/legacy-incompatible-error.ts +26 -0
  82. package/src/core/errors/not-found-error.ts +27 -0
  83. package/src/core/errors/serialization-error.ts +233 -0
  84. package/src/core/errors/temporal-error.ts +145 -0
  85. package/src/core/errors/types.ts +74 -0
  86. package/src/core/errors/unexpected-error.ts +22 -0
  87. package/src/core/errors/utils/index.ts +4 -0
  88. package/src/core/errors/utils/json-safe.ts +93 -0
  89. package/src/core/errors/validation-error.ts +34 -0
  90. package/src/core/exceptions/business-rule-violation-exception.ts +12 -0
  91. package/src/core/exceptions/domain-exception.ts +9 -0
  92. package/src/core/exceptions/entity-not-found-exception.ts +12 -0
  93. package/src/core/exceptions/invalid-state-transition-exception.ts +15 -0
  94. package/src/core/exceptions/invariant-violation-exception.ts +9 -0
  95. package/src/core/exceptions/validation-code.ts +39 -0
  96. package/src/core/exceptions/validation-exception.ts +39 -0
  97. package/src/core/exceptions/validation-field.ts +32 -0
  98. package/src/core/index.ts +15 -0
  99. package/src/core/policies/asof/asof.ts +68 -0
  100. package/src/core/policies/asof/index.ts +2 -0
  101. package/src/core/policies/catalog/catalog-types.ts +12 -0
  102. package/src/core/policies/catalog/catalog.instance.ts +21 -0
  103. package/src/core/policies/catalog/index.ts +11 -0
  104. package/src/core/policies/catalog/policy-catalog-entry.ts +220 -0
  105. package/src/core/policies/catalog/policy-catalog.ts +124 -0
  106. package/src/core/policies/catalog/policy-key.ts +78 -0
  107. package/src/core/policies/context/context-builder.ts +391 -0
  108. package/src/core/policies/context/context-registry.instance.ts +26 -0
  109. package/src/core/policies/context/context-registry.ts +95 -0
  110. package/src/core/policies/context/context-resolver.ts +31 -0
  111. package/src/core/policies/context/context-seed.ts +32 -0
  112. package/src/core/policies/context/index.ts +21 -0
  113. package/src/core/policies/context/path.ts +152 -0
  114. package/src/core/policies/defs/in-memory-policy-definition-repo.ts +73 -0
  115. package/src/core/policies/defs/index.ts +20 -0
  116. package/src/core/policies/defs/policy-definition-repository.ts +8 -0
  117. package/src/core/policies/defs/policy-definition.ts +192 -0
  118. package/src/core/policies/defs/policy-payload.contracts.ts +13 -0
  119. package/src/core/policies/defs/policy-scope.ts +53 -0
  120. package/src/core/policies/defs/policy-semver.ts +83 -0
  121. package/src/core/policies/engines/compute-engine-registry.ts +57 -0
  122. package/src/core/policies/engines/compute-evaluator-registry.ts +28 -0
  123. package/src/core/policies/engines/compute-payload.ts +5 -0
  124. package/src/core/policies/engines/compute-registry.ts +67 -0
  125. package/src/core/policies/engines/compute-types.ts +51 -0
  126. package/src/core/policies/engines/condition-evaluator-reporter.ts +34 -0
  127. package/src/core/policies/engines/gate-engine-registry.ts +38 -0
  128. package/src/core/policies/engines/gate-payload.ts +5 -0
  129. package/src/core/policies/engines/gate-types.ts +63 -0
  130. package/src/core/policies/engines/index.ts +48 -0
  131. package/src/core/policies/engines/parse-compute-payload.ts +71 -0
  132. package/src/core/policies/engines/parse-gate-payload.ts +68 -0
  133. package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +55 -0
  134. package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +75 -0
  135. package/src/core/policies/engines/v1/compute/index.ts +12 -0
  136. package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +54 -0
  137. package/src/core/policies/engines/v1/condition-evaluator.ts +592 -0
  138. package/src/core/policies/engines/v1/condition-schema.ts +50 -0
  139. package/src/core/policies/engines/v1/condition-types.ts +41 -0
  140. package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +157 -0
  141. package/src/core/policies/engines/v1/gate/gate-types-v1.ts +12 -0
  142. package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +46 -0
  143. package/src/core/policies/engines/v1/gate/index.ts +9 -0
  144. package/src/core/policies/engines/v1/index.ts +13 -0
  145. package/src/core/policies/index.ts +132 -0
  146. package/src/core/policies/package/policy-package.ts +17 -0
  147. package/src/core/policies/policy-ids.ts +60 -0
  148. package/src/core/policies/resolver/index.ts +1 -0
  149. package/src/core/policies/resolver/policy-resolver.ts +57 -0
  150. package/src/core/policies/service/index.ts +10 -0
  151. package/src/core/policies/service/policy-evaluation-error.ts +148 -0
  152. package/src/core/policies/service/policy-service.ts +493 -0
  153. package/src/core/policies/utils/date.ts +7 -0
  154. package/src/core/policies/utils/hash.ts +90 -0
  155. package/src/core/policies/utils/index.ts +3 -0
  156. package/src/core/policies/utils/result.ts +4 -0
  157. package/src/core/result/outcome.ts +158 -0
  158. package/src/core/result/result.ts +153 -0
  159. package/src/core/shared/aggregate-version.ts +11 -0
  160. package/src/core/shared/hashing.ts +38 -0
  161. package/src/core/shared/immutable.ts +46 -0
  162. package/src/core/shared/temporal-guards.ts +22 -0
  163. package/src/core/versioning/domain-event-contracts.ts +73 -0
  164. package/src/core/versioning/version.ts +34 -0
  165. package/src/errors/index.ts +1 -0
  166. package/src/examples/rulesets/entity/order-creation-rules-v1.ts +31 -0
  167. package/src/examples/rulesets/entity/order-invariants-v1.ts +48 -0
  168. package/src/examples/rulesets/entity/order-invariants-v2.ts +28 -0
  169. package/src/examples/rulesets/entity/order.ts +162 -0
  170. package/src/examples/rulesets/value-object/cpf-rules-v1.ts +52 -0
  171. package/src/examples/rulesets/value-object/cpf-rules-v2.ts +27 -0
  172. package/src/examples/rulesets/value-object/cpf.ts +37 -0
  173. package/src/examples/rulesets/value-object/customer.ts +67 -0
  174. package/src/examples/rulesets/value-object/person-name-rules-v1.ts +37 -0
  175. package/src/examples/rulesets/value-object/person-name-rules-v2.ts +27 -0
  176. package/src/examples/rulesets/value-object/person-name.ts +39 -0
  177. package/src/exceptions/validation-field.ts +1 -0
  178. package/src/index.ts +11 -0
  179. package/src/policies/engines/index.ts +1 -0
  180. package/src/policies/engines/v1/gate/index.ts +1 -0
  181. package/src/policies/index.ts +1 -0
  182. package/src/version.ts +7 -0
@@ -0,0 +1,493 @@
1
+ import { PolicyAsOfResolver, type DeriveAsOfOptions } from "../asof";
2
+ import { type PolicyCatalog, PolicyCatalogEntry, PolicyKey } from "../catalog";
3
+ import type { ContextSeed, PolicyContextBuilder } from "../context";
4
+ import { ContextSeedValidator } from "../context";
5
+ import {
6
+ PolicyDefinition,
7
+ type PolicyDefinitionRepository,
8
+ type PolicyScope,
9
+ } from "../defs";
10
+ import type { PolicyDecisionId, PolicyDefinitionId } from "../policy-ids";
11
+ import {
12
+ type ComputeOutcome,
13
+ ComputeRegistry,
14
+ GateEngineRegistry,
15
+ type GateOutcome,
16
+ type PolicyContext,
17
+ } from "../engines";
18
+ import { PolicyResolver } from "../resolver";
19
+ import { Result } from "../../result/result";
20
+ import { isValidDate } from "../../shared/temporal-guards";
21
+
22
+ import type {
23
+ PolicyEvent,
24
+ PolicyReporter,
25
+ } from "../../config/policy-reporter";
26
+ import { SilentPolicyReporter } from "../../config/silent-policy-reporter";
27
+
28
+ import {
29
+ PolicyEvaluationErrors,
30
+ type PolicyEvaluationError,
31
+ } from "./policy-evaluation-error";
32
+
33
+ // ─── Input ──────────────────────────────────────────────────────────────────
34
+
35
+ export interface EvaluateInput {
36
+ readonly decisionId: PolicyDecisionId;
37
+ readonly policyKey: string;
38
+ readonly scopeChain: readonly PolicyScope[];
39
+ readonly contextVersion: number;
40
+ readonly seed: ContextSeed;
41
+ }
42
+
43
+ export interface PolicyServiceOptions {
44
+ readonly asOf?: DeriveAsOfOptions;
45
+ readonly reporter?: PolicyReporter;
46
+ }
47
+
48
+ export interface PolicyServiceParams {
49
+ readonly catalog: PolicyCatalog;
50
+ readonly contextBuilder: PolicyContextBuilder;
51
+ readonly defRepo: PolicyDefinitionRepository;
52
+ readonly resolver: PolicyResolver;
53
+ readonly gateEngines: GateEngineRegistry;
54
+ readonly computeRegistry: ComputeRegistry;
55
+ readonly options?: PolicyServiceOptions;
56
+ }
57
+
58
+ // ─── Output ─────────────────────────────────────────────────────────────────
59
+
60
+ /** Union of all possible policy decision Outcomes. */
61
+ export type PolicyDecision = GateOutcome | ComputeOutcome;
62
+
63
+ export type { PolicyEvaluationError } from "./policy-evaluation-error";
64
+
65
+ export interface PolicyEvaluationResult {
66
+ readonly decisionId: PolicyDecisionId;
67
+ readonly ref: {
68
+ readonly definitionId: PolicyDefinitionId;
69
+ readonly policyKey: string;
70
+ readonly policyVersion: string;
71
+ readonly payloadHash: string;
72
+ readonly gateEngineVersion?: number;
73
+ readonly computeEngineVersion?: number;
74
+ };
75
+ readonly kind: "GATE" | "COMPUTE";
76
+ readonly asOf: Date;
77
+ readonly evaluatedAt: Date;
78
+ readonly contextVersion: number;
79
+ /** Business decision expressed as an Outcome — not a technical Result. */
80
+ readonly decision: PolicyDecision;
81
+ }
82
+
83
+ interface ResolvedPolicyCandidate {
84
+ readonly definition: PolicyDefinition;
85
+ readonly catalogEntry: PolicyCatalogEntry;
86
+ }
87
+
88
+ // ─── Service ────────────────────────────────────────────────────────────────
89
+
90
+ export class PolicyService {
91
+ readonly #reporter: PolicyReporter;
92
+ private readonly catalog: PolicyCatalog;
93
+ private readonly contextBuilder: PolicyContextBuilder;
94
+ private readonly defRepo: PolicyDefinitionRepository;
95
+ private readonly resolver: PolicyResolver;
96
+ private readonly gateEngines: GateEngineRegistry;
97
+ private readonly computeRegistry: ComputeRegistry;
98
+ private readonly options: PolicyServiceOptions;
99
+
100
+ constructor(params: PolicyServiceParams) {
101
+ this.catalog = params.catalog;
102
+ this.contextBuilder = params.contextBuilder;
103
+ this.defRepo = params.defRepo;
104
+ this.resolver = params.resolver;
105
+ this.gateEngines = params.gateEngines;
106
+ this.computeRegistry = params.computeRegistry;
107
+ this.options = params.options ?? {};
108
+ this.#reporter = this.options.reporter ?? new SilentPolicyReporter();
109
+ }
110
+
111
+ #reportSafely(event: PolicyEvent): void {
112
+ try {
113
+ this.#reporter.report(event);
114
+ } catch {
115
+ // Falhas de telemetria nao podem interromper a avaliacao de politicas.
116
+ }
117
+ }
118
+
119
+ private resolveEvaluationNow(seed: ContextSeed): Result<Date, string> {
120
+ const candidate = seed.fields["now"];
121
+
122
+ if (candidate === undefined) {
123
+ return Result.ok(new Date());
124
+ }
125
+
126
+ if (!isValidDate(candidate)) {
127
+ return Result.err("seed.fields.now must be a valid Date when provided");
128
+ }
129
+
130
+ return Result.ok(candidate);
131
+ }
132
+
133
+ async evaluate(
134
+ input: EvaluateInput,
135
+ ): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
136
+ const result = await this.#evaluate(input);
137
+ if (result.isErr()) {
138
+ const error = result.errorOrNull()!;
139
+ this.#reportSafely({
140
+ kind: "policy-evaluation-failed",
141
+ level: "error",
142
+ policyKey: "policyKey" in error ? error.policyKey : undefined,
143
+ errorKind: error.kind,
144
+ message: error.message,
145
+ cause: error.cause,
146
+ occurredAt: new Date(),
147
+ });
148
+ } else {
149
+ const ok = result.getOrNull()!;
150
+ this.#reportSafely({
151
+ kind: "policy-evaluation-completed",
152
+ level: "info",
153
+ policyKey: ok.ref.policyKey,
154
+ decisionId: ok.decisionId,
155
+ engineKind: ok.kind,
156
+ occurredAt: ok.evaluatedAt,
157
+ });
158
+ }
159
+ return result;
160
+ }
161
+
162
+ async #evaluate(
163
+ input: EvaluateInput,
164
+ ): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
165
+ const seedResult = ContextSeedValidator.validate(input.seed);
166
+ if (seedResult.isErr()) {
167
+ return Result.err(
168
+ PolicyEvaluationErrors.invalidContext(
169
+ "SEED_VALIDATION",
170
+ input.policyKey,
171
+ seedResult.errorOrNull()!,
172
+ ),
173
+ );
174
+ }
175
+ const seed = seedResult.getOrNull()!;
176
+
177
+ const catalogFamilyResult = this.#resolveCatalogFamily(input.policyKey);
178
+ if (catalogFamilyResult.isErr()) {
179
+ return Result.err(catalogFamilyResult.errorOrNull()!);
180
+ }
181
+ const { key: policyKey, family } = catalogFamilyResult.getOrNull()!;
182
+ const familyEntry = family[0]!;
183
+
184
+ const nowResult = this.resolveEvaluationNow(seed);
185
+ if (nowResult.isErr()) {
186
+ return Result.err(
187
+ PolicyEvaluationErrors.invalidContext(
188
+ "AS_OF_DERIVATION",
189
+ policyKey,
190
+ nowResult.errorOrNull()!,
191
+ ),
192
+ );
193
+ }
194
+ const now = nowResult.getOrNull()!;
195
+
196
+ const asOfResult = PolicyAsOfResolver.derive(
197
+ familyEntry.asOfSource,
198
+ seed,
199
+ now,
200
+ this.options.asOf,
201
+ );
202
+ if (asOfResult.isErr()) {
203
+ return Result.err(
204
+ PolicyEvaluationErrors.invalidContext(
205
+ "AS_OF_DERIVATION",
206
+ policyKey,
207
+ asOfResult.errorOrNull()!,
208
+ ),
209
+ );
210
+ }
211
+ const asOf = asOfResult.getOrNull()!;
212
+
213
+ const definitionResult = this.#findCandidates(
214
+ policyKey,
215
+ familyEntry.kind,
216
+ asOf,
217
+ input,
218
+ );
219
+ if (definitionResult.isErr()) {
220
+ return Result.err(definitionResult.errorOrNull()!);
221
+ }
222
+ const {
223
+ definition: policyDefinition,
224
+ catalogEntry: versionedCatalogEntry,
225
+ } = definitionResult.getOrNull()!;
226
+
227
+ this.#reportSafely({
228
+ kind: "policy-resolution",
229
+ level: "info",
230
+ policyKey,
231
+ definitionId: policyDefinition.id,
232
+ policyVersion: policyDefinition.policyVersion,
233
+ engineKind: policyDefinition.isGate() ? "GATE" : "COMPUTE",
234
+ engineVersion: policyDefinition.isGate()
235
+ ? policyDefinition.gateEngineVersion!
236
+ : policyDefinition.computeEngineVersion!,
237
+ occurredAt: now,
238
+ });
239
+
240
+ const ctxResult = await this.contextBuilder.build(
241
+ versionedCatalogEntry.contextRequirements,
242
+ seed,
243
+ );
244
+ if (ctxResult.isErr()) {
245
+ return Result.err(
246
+ PolicyEvaluationErrors.invalidContext(
247
+ "CONTEXT_BUILD",
248
+ policyKey,
249
+ ctxResult.errorOrNull()!,
250
+ ),
251
+ );
252
+ }
253
+ const context = ctxResult.getOrNull()!;
254
+
255
+ const decisionResult = this.#executeEngine(
256
+ policyKey,
257
+ policyDefinition,
258
+ context,
259
+ );
260
+ if (decisionResult.isErr()) {
261
+ return Result.err(decisionResult.errorOrNull()!);
262
+ }
263
+ const decision = decisionResult.getOrNull()!;
264
+
265
+ const result: PolicyEvaluationResult = {
266
+ decisionId: input.decisionId,
267
+ ref: {
268
+ definitionId: policyDefinition.id,
269
+ policyKey: policyDefinition.policyKey,
270
+ policyVersion: policyDefinition.policyVersion,
271
+ payloadHash: policyDefinition.payloadHash,
272
+ ...(policyDefinition.isGate()
273
+ ? {
274
+ gateEngineVersion: policyDefinition.gateEngineVersion,
275
+ }
276
+ : {
277
+ computeEngineVersion: policyDefinition.computeEngineVersion,
278
+ }),
279
+ },
280
+ kind: policyDefinition.kind,
281
+ asOf,
282
+ evaluatedAt: now,
283
+ contextVersion: input.contextVersion,
284
+ decision,
285
+ };
286
+
287
+ return Result.ok(result);
288
+ }
289
+
290
+ #resolveCatalogVariant(
291
+ definition: PolicyDefinition,
292
+ ): Result<PolicyCatalogEntry, string> {
293
+ return this.catalog.getVersioned({
294
+ key: definition.policyKey,
295
+ kind: definition.kind,
296
+ ...(definition.isGate()
297
+ ? { gateEngineVersion: definition.gateEngineVersion }
298
+ : {
299
+ computeEngineVersion: definition.computeEngineVersion,
300
+ }),
301
+ payloadSchemaVersion: definition.payloadSchemaVersion,
302
+ });
303
+ }
304
+
305
+ #resolveCatalogFamily(
306
+ rawKey: string,
307
+ ): Result<
308
+ { key: string; family: readonly PolicyCatalogEntry[] },
309
+ PolicyEvaluationError
310
+ > {
311
+ const keyResult = PolicyKey.parse(rawKey);
312
+ if (keyResult.isErr()) {
313
+ return Result.err(
314
+ PolicyEvaluationErrors.invalidPolicyKey(
315
+ rawKey,
316
+ keyResult.errorOrNull()!,
317
+ ),
318
+ );
319
+ }
320
+ const policyKey = keyResult.getOrNull()!.toString();
321
+
322
+ const catalogFamilyResult = this.catalog.getFamily(policyKey);
323
+ if (catalogFamilyResult.isErr()) {
324
+ return Result.err(
325
+ PolicyEvaluationErrors.policyNotFound(
326
+ policyKey,
327
+ catalogFamilyResult.errorOrNull()!,
328
+ ),
329
+ );
330
+ }
331
+
332
+ return Result.ok({
333
+ key: policyKey,
334
+ family: catalogFamilyResult.getOrNull()!,
335
+ });
336
+ }
337
+
338
+ #findCandidates(
339
+ policyKey: string,
340
+ policyKind: PolicyCatalogEntry["kind"],
341
+ asOf: Date,
342
+ input: EvaluateInput,
343
+ ): Result<ResolvedPolicyCandidate, PolicyEvaluationError> {
344
+ const candidates = this.defRepo.findCandidates({
345
+ policyKey: policyKey,
346
+ kind: policyKind,
347
+ asOf,
348
+ contextVersion: input.contextVersion,
349
+ scopeChain: input.scopeChain,
350
+ });
351
+
352
+ const compatibleCandidates: ResolvedPolicyCandidate[] = [];
353
+ const compatibleCandidatesByDefinitionId = new Map<
354
+ string,
355
+ ResolvedPolicyCandidate
356
+ >();
357
+ const incompatibleVariantErrorsByDefinitionId = new Map<string, string>();
358
+
359
+ for (const candidate of candidates) {
360
+ const compatibilityResult = this.#resolveCatalogVariant(candidate);
361
+ if (compatibilityResult.isErr()) {
362
+ incompatibleVariantErrorsByDefinitionId.set(
363
+ candidate.id,
364
+ compatibilityResult.errorOrNull()!,
365
+ );
366
+ continue;
367
+ }
368
+
369
+ const resolvedCandidate: ResolvedPolicyCandidate = {
370
+ definition: candidate,
371
+ catalogEntry: compatibilityResult.getOrNull()!,
372
+ };
373
+ compatibleCandidates.push(resolvedCandidate);
374
+ compatibleCandidatesByDefinitionId.set(candidate.id, resolvedCandidate);
375
+ }
376
+
377
+ if (compatibleCandidates.length === 0 && candidates.length > 0) {
378
+ const selectedCandidateResult = this.resolver.resolveBest(candidates);
379
+ if (selectedCandidateResult.isOk()) {
380
+ const selectedCandidate = selectedCandidateResult.getOrNull()!;
381
+
382
+ return Result.err(
383
+ PolicyEvaluationErrors.policyVariantNotFound({
384
+ policyKey: policyKey,
385
+ policyKind: selectedCandidate.kind,
386
+ payloadSchemaVersion: selectedCandidate.payloadSchemaVersion,
387
+ ...(selectedCandidate.isGate()
388
+ ? {
389
+ gateEngineVersion: selectedCandidate.gateEngineVersion,
390
+ }
391
+ : {
392
+ computeEngineVersion: selectedCandidate.computeEngineVersion,
393
+ }),
394
+ cause:
395
+ incompatibleVariantErrorsByDefinitionId.get(
396
+ selectedCandidate.id,
397
+ ) ??
398
+ "No compatible catalog variant matched the selected policy definition",
399
+ }),
400
+ );
401
+ }
402
+ }
403
+
404
+ const bestResult = this.resolver.resolveBest(
405
+ compatibleCandidates.map((candidate) => candidate.definition),
406
+ );
407
+ if (bestResult.isErr()) {
408
+ return Result.err(
409
+ PolicyEvaluationErrors.policyDefinitionNotFound(
410
+ policyKey,
411
+ asOf,
412
+ input.contextVersion,
413
+ bestResult.errorOrNull()!,
414
+ ),
415
+ );
416
+ }
417
+
418
+ const bestDefinition = bestResult.getOrNull()!;
419
+ const resolvedCandidate = compatibleCandidatesByDefinitionId.get(
420
+ bestDefinition.id,
421
+ );
422
+ if (resolvedCandidate === undefined) {
423
+ return Result.err(
424
+ PolicyEvaluationErrors.policyDefinitionNotFound(
425
+ policyKey,
426
+ asOf,
427
+ input.contextVersion,
428
+ `Resolved definition "${bestDefinition.id}" is not present among compatible catalog variants`,
429
+ ),
430
+ );
431
+ }
432
+
433
+ return Result.ok(resolvedCandidate);
434
+ }
435
+
436
+ #executeEngine(
437
+ policyKey: string,
438
+ definition: PolicyDefinition,
439
+ context: PolicyContext,
440
+ ): Result<PolicyDecision, PolicyEvaluationError> {
441
+ if (definition.isGate()) {
442
+ const gateResult = this.gateEngines.evaluate(
443
+ definition.gateEngineVersion,
444
+ definition.payloadJson,
445
+ context,
446
+ );
447
+ if (gateResult.isErr()) {
448
+ return Result.err(
449
+ PolicyEvaluationErrors.engineFailure(
450
+ policyKey,
451
+ "GATE",
452
+ definition.gateEngineVersion,
453
+ gateResult.errorOrNull()!,
454
+ ),
455
+ );
456
+ }
457
+
458
+ return Result.ok(gateResult.getOrNull()!);
459
+ }
460
+
461
+ if (!definition.isCompute()) {
462
+ return Result.err(
463
+ PolicyEvaluationErrors.policyVariantNotFound({
464
+ policyKey,
465
+ policyKind: definition.kind,
466
+ payloadSchemaVersion: definition.payloadSchemaVersion,
467
+ cause: `Unsupported policy kind "${definition.kind}"`,
468
+ }),
469
+ );
470
+ }
471
+
472
+ const computeResult = this.computeRegistry.evaluate(
473
+ policyKey,
474
+ definition.computeEngineVersion,
475
+ definition.payloadSchemaVersion,
476
+ definition.payloadJson,
477
+ context,
478
+ );
479
+
480
+ if (computeResult.isErr()) {
481
+ return Result.err(
482
+ PolicyEvaluationErrors.engineFailure(
483
+ policyKey,
484
+ "COMPUTE",
485
+ definition.computeEngineVersion,
486
+ computeResult.errorOrNull()!,
487
+ ),
488
+ );
489
+ }
490
+
491
+ return Result.ok(computeResult.getOrNull()!);
492
+ }
493
+ }
@@ -0,0 +1,7 @@
1
+ import { isValidDate } from "../../shared/temporal-guards";
2
+
3
+ export class PolicyDateUtils {
4
+ static isValid(value: Date): boolean {
5
+ return isValidDate(value);
6
+ }
7
+ }
@@ -0,0 +1,90 @@
1
+ import { sha256Hex, stableStringify } from "../../shared/hashing";
2
+ import { isValidDate } from "../../shared/temporal-guards";
3
+
4
+ /**
5
+ * Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
6
+ * Object keys are sorted recursively so key order does not affect the hash.
7
+ * Array item order is preserved on purpose; callers that treat arrays as sets
8
+ * must normalize or sort them before hashing.
9
+ *
10
+ * Values that JSON.stringify would silently drop or coerce (undefined,
11
+ * non-finite numbers, functions, symbols, bigint) are rejected up-front to
12
+ * prevent semantically different payloads from collapsing to the same hash.
13
+ */
14
+ export class PolicyHashing {
15
+ static sha256(input: string): string {
16
+ return sha256Hex(input);
17
+ }
18
+
19
+ private static assertHashable(value: unknown, path: string): void {
20
+ if (value === null) {
21
+ return;
22
+ }
23
+
24
+ const type = typeof value;
25
+
26
+ if (type === "undefined") {
27
+ throw new TypeError(
28
+ `canonicalJson does not accept undefined values (at ${path})`,
29
+ );
30
+ }
31
+
32
+ if (type === "number" && !Number.isFinite(value)) {
33
+ throw new TypeError(
34
+ `canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`,
35
+ );
36
+ }
37
+
38
+ if (type === "bigint" || type === "function" || type === "symbol") {
39
+ throw new TypeError(
40
+ `canonicalJson does not accept ${type} values (at ${path})`,
41
+ );
42
+ }
43
+
44
+ if (type !== "object") {
45
+ return;
46
+ }
47
+
48
+ if (value instanceof Date) {
49
+ if (!isValidDate(value)) {
50
+ throw new TypeError(
51
+ `canonicalJson does not accept Invalid Date (at ${path})`,
52
+ );
53
+ }
54
+ return;
55
+ }
56
+
57
+ if (Array.isArray(value)) {
58
+ value.forEach((item, index) => {
59
+ PolicyHashing.assertHashable(item, `${path}[${index}]`);
60
+ });
61
+ return;
62
+ }
63
+
64
+ for (const [key, nested] of Object.entries(
65
+ value as Record<string, unknown>,
66
+ )) {
67
+ PolicyHashing.assertHashable(nested, `${path}.${key}`);
68
+ }
69
+ }
70
+
71
+ static canonicalJson(value: unknown): string {
72
+ PolicyHashing.assertHashable(value, "$");
73
+
74
+ return stableStringify(value);
75
+ }
76
+
77
+ static computePayloadHash(
78
+ payload: unknown,
79
+ policyKey: string,
80
+ policyVersion: string,
81
+ ): string {
82
+ const canonical = PolicyHashing.canonicalJson({
83
+ policyKey,
84
+ policyVersion,
85
+ payload,
86
+ });
87
+
88
+ return PolicyHashing.sha256(canonical);
89
+ }
90
+ }
@@ -0,0 +1,3 @@
1
+ export { Result, Ok, Err } from "./result";
2
+ export { PolicyHashing } from "./hash";
3
+ export { PolicyDateUtils } from "./date";
@@ -0,0 +1,4 @@
1
+ // Re-export the core Result class.
2
+ // The policies module uses Result<T, string> as its standard Result type.
3
+
4
+ export { Err, Ok, Result } from "../../result/result";