@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,391 @@
1
+ import { Result } from "../../result/result";
2
+
3
+ import type { ContextResolverRegistry } from "./context-registry";
4
+ import type {
5
+ ContextResolverResilienceOptions,
6
+ ContextResolverRetryOptions,
7
+ ContextValueResolver,
8
+ } from "./context-resolver";
9
+ import type { ContextSeed } from "./context-seed";
10
+ import { PolicyContextPath } from "./path";
11
+
12
+ interface NormalizedContextResolverRetryOptions {
13
+ readonly maxAttempts: number;
14
+ readonly initialDelayMs: number;
15
+ readonly maxDelayMs: number;
16
+ readonly backoffMultiplier: number;
17
+ }
18
+
19
+ interface NormalizedContextResolverCircuitBreakerOptions {
20
+ readonly failureThreshold: number;
21
+ readonly cooldownMs: number;
22
+ }
23
+
24
+ interface NormalizedContextResolverResilienceOptions {
25
+ readonly timeoutMs: number | null;
26
+ readonly retry: NormalizedContextResolverRetryOptions;
27
+ readonly circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null;
28
+ }
29
+
30
+ interface ContextResolverCircuitState {
31
+ readonly failureCount: number;
32
+ readonly openUntilMs: number | null;
33
+ }
34
+
35
+ export interface PolicyContextBuilderOptions {
36
+ readonly defaultResolverResilience?: ContextResolverResilienceOptions;
37
+ readonly now?: () => number;
38
+ readonly sleep?: (delayMs: number) => Promise<void>;
39
+ }
40
+
41
+ const DEFAULT_RESOLVER_RETRY_OPTIONS: NormalizedContextResolverRetryOptions = {
42
+ maxAttempts: 1,
43
+ initialDelayMs: 25,
44
+ maxDelayMs: 1_000,
45
+ backoffMultiplier: 2,
46
+ };
47
+
48
+ /**
49
+ * Builds a context object by resolving required paths from a seed.
50
+ */
51
+ export class PolicyContextBuilder {
52
+ private readonly circuitStates = new Map<
53
+ string,
54
+ ContextResolverCircuitState
55
+ >();
56
+ private readonly defaultResolverResilience?: ContextResolverResilienceOptions;
57
+ private readonly now: () => number;
58
+ private readonly sleep: (delayMs: number) => Promise<void>;
59
+
60
+ constructor(
61
+ private readonly registry: ContextResolverRegistry,
62
+ options: PolicyContextBuilderOptions = {},
63
+ ) {
64
+ this.defaultResolverResilience = options.defaultResolverResilience;
65
+ this.now = options.now ?? Date.now;
66
+ this.sleep =
67
+ options.sleep ??
68
+ (async (delayMs) => {
69
+ if (delayMs <= 0) {
70
+ return;
71
+ }
72
+
73
+ await new Promise<void>((resolve) => {
74
+ setTimeout(resolve, delayMs);
75
+ });
76
+ });
77
+ }
78
+
79
+ private static clampPositiveInteger(
80
+ value: number | undefined,
81
+ fallback: number,
82
+ ): number {
83
+ if (value === undefined || !Number.isFinite(value)) {
84
+ return fallback;
85
+ }
86
+
87
+ return Math.max(1, Math.trunc(value));
88
+ }
89
+
90
+ private static clampNonNegativeInteger(
91
+ value: number | undefined,
92
+ fallback: number,
93
+ ): number {
94
+ if (value === undefined || !Number.isFinite(value)) {
95
+ return fallback;
96
+ }
97
+
98
+ return Math.max(0, Math.trunc(value));
99
+ }
100
+
101
+ private static normalizeTimeoutMs(value: number | undefined): number | null {
102
+ if (value === undefined || !Number.isFinite(value) || value <= 0) {
103
+ return null;
104
+ }
105
+
106
+ return Math.trunc(value);
107
+ }
108
+
109
+ private static mergeRetryOptions(
110
+ defaults: ContextResolverRetryOptions | false | undefined,
111
+ overrides: ContextResolverRetryOptions | false | undefined,
112
+ ): ContextResolverRetryOptions | false | undefined {
113
+ if (overrides === false) {
114
+ return false;
115
+ }
116
+
117
+ if (overrides !== undefined) {
118
+ return {
119
+ ...(defaults === false || defaults === undefined ? {} : defaults),
120
+ ...overrides,
121
+ };
122
+ }
123
+
124
+ return defaults;
125
+ }
126
+
127
+ private static mergeCircuitBreakerOptions(
128
+ defaults: ContextResolverResilienceOptions["circuitBreaker"] | undefined,
129
+ overrides: ContextResolverResilienceOptions["circuitBreaker"] | undefined,
130
+ ): ContextResolverResilienceOptions["circuitBreaker"] | undefined {
131
+ if (overrides === false) {
132
+ return false;
133
+ }
134
+
135
+ if (overrides !== undefined) {
136
+ return {
137
+ ...(defaults === false || defaults === undefined ? {} : defaults),
138
+ ...overrides,
139
+ };
140
+ }
141
+
142
+ return defaults;
143
+ }
144
+
145
+ private resolveResilienceOptions(
146
+ resolver: ContextValueResolver,
147
+ ): NormalizedContextResolverResilienceOptions {
148
+ const defaults = this.defaultResolverResilience;
149
+ const override = resolver.resilience;
150
+ const retry = PolicyContextBuilder.mergeRetryOptions(
151
+ defaults?.retry,
152
+ override?.retry,
153
+ );
154
+ const circuitBreaker = PolicyContextBuilder.mergeCircuitBreakerOptions(
155
+ defaults?.circuitBreaker,
156
+ override?.circuitBreaker,
157
+ );
158
+ const retryOptions = retry === false ? undefined : retry;
159
+ const circuitBreakerOptions =
160
+ circuitBreaker === false ? undefined : circuitBreaker;
161
+
162
+ return {
163
+ timeoutMs: PolicyContextBuilder.normalizeTimeoutMs(
164
+ override?.timeoutMs ?? defaults?.timeoutMs,
165
+ ),
166
+ retry: {
167
+ maxAttempts: PolicyContextBuilder.clampPositiveInteger(
168
+ retryOptions?.maxAttempts,
169
+ DEFAULT_RESOLVER_RETRY_OPTIONS.maxAttempts,
170
+ ),
171
+ initialDelayMs: PolicyContextBuilder.clampNonNegativeInteger(
172
+ retryOptions?.initialDelayMs,
173
+ DEFAULT_RESOLVER_RETRY_OPTIONS.initialDelayMs,
174
+ ),
175
+ maxDelayMs: PolicyContextBuilder.clampNonNegativeInteger(
176
+ retryOptions?.maxDelayMs,
177
+ DEFAULT_RESOLVER_RETRY_OPTIONS.maxDelayMs,
178
+ ),
179
+ backoffMultiplier: PolicyContextBuilder.clampPositiveInteger(
180
+ retryOptions?.backoffMultiplier,
181
+ DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier,
182
+ ),
183
+ },
184
+ circuitBreaker:
185
+ circuitBreakerOptions === undefined
186
+ ? null
187
+ : {
188
+ failureThreshold: PolicyContextBuilder.clampPositiveInteger(
189
+ circuitBreakerOptions.failureThreshold,
190
+ 5,
191
+ ),
192
+ cooldownMs: PolicyContextBuilder.clampPositiveInteger(
193
+ circuitBreakerOptions.cooldownMs,
194
+ 30_000,
195
+ ),
196
+ },
197
+ };
198
+ }
199
+
200
+ private getRetryDelayMs(
201
+ attempt: number,
202
+ retry: NormalizedContextResolverRetryOptions,
203
+ ): number {
204
+ return Math.min(
205
+ retry.initialDelayMs * retry.backoffMultiplier ** (attempt - 1),
206
+ retry.maxDelayMs,
207
+ );
208
+ }
209
+
210
+ private resetCircuit(path: string): void {
211
+ this.circuitStates.delete(path);
212
+ }
213
+
214
+ private isCircuitOpen(
215
+ path: string,
216
+ circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null,
217
+ ): boolean {
218
+ if (circuitBreaker === null) {
219
+ return false;
220
+ }
221
+
222
+ const state = this.circuitStates.get(path);
223
+ if (state === undefined || state.openUntilMs === null) {
224
+ return false;
225
+ }
226
+
227
+ if (state.openUntilMs <= this.now()) {
228
+ this.resetCircuit(path);
229
+ return false;
230
+ }
231
+
232
+ return true;
233
+ }
234
+
235
+ private recordResolverFailure(
236
+ path: string,
237
+ circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null,
238
+ ): void {
239
+ if (circuitBreaker === null) {
240
+ return;
241
+ }
242
+
243
+ const currentFailureCount = this.circuitStates.get(path)?.failureCount ?? 0;
244
+ const nextFailureCount = currentFailureCount + 1;
245
+
246
+ if (nextFailureCount >= circuitBreaker.failureThreshold) {
247
+ this.circuitStates.set(path, {
248
+ failureCount: 0,
249
+ openUntilMs: this.now() + circuitBreaker.cooldownMs,
250
+ });
251
+ return;
252
+ }
253
+
254
+ this.circuitStates.set(path, {
255
+ failureCount: nextFailureCount,
256
+ openUntilMs: null,
257
+ });
258
+ }
259
+
260
+ private static describeThrownResolverError(error: unknown): string {
261
+ if (error instanceof Error) {
262
+ return `threw ${error.name}: ${error.message}`;
263
+ }
264
+
265
+ return `threw ${String(error)}`;
266
+ }
267
+
268
+ private async resolveOnce(
269
+ resolver: ContextValueResolver,
270
+ seed: ContextSeed,
271
+ timeoutMs: number | null,
272
+ ): Promise<Result<unknown, string>> {
273
+ const execute = async (): Promise<Result<unknown, string>> => {
274
+ try {
275
+ return await resolver.resolve(seed);
276
+ } catch (error) {
277
+ return Result.err(
278
+ PolicyContextBuilder.describeThrownResolverError(error),
279
+ );
280
+ }
281
+ };
282
+
283
+ if (timeoutMs === null) {
284
+ return execute();
285
+ }
286
+
287
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
288
+
289
+ try {
290
+ return await Promise.race([
291
+ execute(),
292
+ new Promise<Result<unknown, string>>((resolve) => {
293
+ timeoutId = setTimeout(() => {
294
+ resolve(Result.err(`timed out after ${timeoutMs}ms`));
295
+ }, timeoutMs);
296
+ }),
297
+ ]);
298
+ } finally {
299
+ if (timeoutId !== undefined) {
300
+ clearTimeout(timeoutId);
301
+ }
302
+ }
303
+ }
304
+
305
+ private async resolveWithResilience(
306
+ path: string,
307
+ resolver: ContextValueResolver,
308
+ seed: ContextSeed,
309
+ ): Promise<Result<unknown, string>> {
310
+ const resilience = this.resolveResilienceOptions(resolver);
311
+
312
+ if (this.isCircuitOpen(path, resilience.circuitBreaker)) {
313
+ return Result.err("circuit breaker is open");
314
+ }
315
+
316
+ let lastError = "resolver failed";
317
+
318
+ for (let attempt = 1; attempt <= resilience.retry.maxAttempts; attempt++) {
319
+ const result = await this.resolveOnce(
320
+ resolver,
321
+ seed,
322
+ resilience.timeoutMs,
323
+ );
324
+
325
+ if (result.isOk()) {
326
+ this.resetCircuit(path);
327
+ return result;
328
+ }
329
+
330
+ lastError = result.errorOrNull() ?? lastError;
331
+
332
+ if (attempt < resilience.retry.maxAttempts) {
333
+ await this.sleep(this.getRetryDelayMs(attempt, resilience.retry));
334
+ }
335
+ }
336
+
337
+ this.recordResolverFailure(path, resilience.circuitBreaker);
338
+ return Result.err(lastError);
339
+ }
340
+
341
+ /**
342
+ * Resolves all required context paths and returns a flat context object.
343
+ * If any required path cannot be resolved, returns an error.
344
+ */
345
+ async build(
346
+ requirements: readonly string[],
347
+ seed: ContextSeed,
348
+ ): Promise<Result<Record<string, unknown>, string>> {
349
+ const context: Record<string, unknown> = {};
350
+
351
+ for (const path of requirements) {
352
+ const resolver = this.registry.get(path);
353
+ if (!resolver) {
354
+ return Result.err(`Missing resolver for path "${path}"`);
355
+ }
356
+
357
+ const result = await this.resolveWithResilience(path, resolver, seed);
358
+ if (result.isErr()) {
359
+ return Result.err(
360
+ `Resolver error for path "${path}": ${result.errorOrNull()}`,
361
+ );
362
+ }
363
+
364
+ const setPathResult = PolicyContextPath.set(
365
+ context,
366
+ path,
367
+ result.getOrNull(),
368
+ );
369
+ if (setPathResult.isErr()) {
370
+ return Result.err(setPathResult.errorOrNull()!);
371
+ }
372
+ }
373
+
374
+ // Final validation: ensure every required path resolved to a defined value.
375
+ // `undefined` is rejected explicitly because resolvers that "succeed" with
376
+ // undefined indicate an unresolved field, not a present-but-empty one.
377
+ const missing = requirements.filter((path) => {
378
+ const result = PolicyContextPath.getOrAbsent(context, path);
379
+ return result.isErr() || result.getOrNull() === undefined;
380
+ });
381
+ if (missing.length > 0) {
382
+ return Result.err(
383
+ `Context missing required paths after resolution: ${missing.join(
384
+ ", ",
385
+ )}`,
386
+ );
387
+ }
388
+
389
+ return Result.ok(context);
390
+ }
391
+ }
@@ -0,0 +1,26 @@
1
+ import { Result } from "../../result/result";
2
+
3
+ import {
4
+ ContextResolverRegistry,
5
+ registerNamespacedContextResolversIn,
6
+ } from "./context-registry";
7
+
8
+ import type { ContextValueResolver } from "./context-resolver";
9
+
10
+ /**
11
+ * Official instance of the ContextResolverRegistry.
12
+ * Use `new ContextResolverRegistry()` plus `registerNamespacedContextResolversIn`
13
+ * when each runtime/composition root needs its own isolated resolver graph.
14
+ */
15
+ export const contextResolverRegistry = new ContextResolverRegistry();
16
+
17
+ export function registerNamespacedContextResolvers(
18
+ namespace: string,
19
+ resolvers: readonly ContextValueResolver[],
20
+ ): Result<ContextResolverRegistry, string> {
21
+ return registerNamespacedContextResolversIn(
22
+ contextResolverRegistry,
23
+ namespace,
24
+ resolvers,
25
+ );
26
+ }
@@ -0,0 +1,95 @@
1
+ import { Result } from "../../result/result";
2
+
3
+ import type { ContextValueResolver } from "./context-resolver";
4
+
5
+ export interface ContextResolverRegistrationOptions {
6
+ readonly namespace?: string;
7
+ }
8
+
9
+ interface RegisteredContextResolver {
10
+ readonly namespace: string;
11
+ readonly resolver: ContextValueResolver;
12
+ }
13
+
14
+ const DEFAULT_CONTEXT_RESOLVER_NAMESPACE = "default";
15
+
16
+ function normalizeNamespace(namespace?: string): string {
17
+ const normalized = namespace?.trim();
18
+
19
+ if (normalized === undefined || normalized.length === 0) {
20
+ return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
21
+ }
22
+
23
+ return normalized;
24
+ }
25
+
26
+ /**
27
+ * Registry of context value resolvers, keyed by path.
28
+ */
29
+ export class ContextResolverRegistry {
30
+ private readonly resolvers = new Map<string, RegisteredContextResolver>();
31
+
32
+ register(
33
+ resolver: ContextValueResolver,
34
+ options: ContextResolverRegistrationOptions = {},
35
+ ): Result<void, string> {
36
+ const namespace = normalizeNamespace(options.namespace);
37
+ const existing = this.resolvers.get(resolver.path);
38
+
39
+ if (existing !== undefined && existing.namespace !== namespace) {
40
+ return Result.err(
41
+ `Resolver path "${resolver.path}" is already registered by namespace "${existing.namespace}" and cannot be replaced by "${namespace}".`,
42
+ );
43
+ }
44
+
45
+ this.resolvers.set(resolver.path, {
46
+ namespace,
47
+ resolver,
48
+ });
49
+
50
+ return Result.ok(undefined);
51
+ }
52
+
53
+ registerAll(
54
+ resolvers: readonly ContextValueResolver[],
55
+ options: ContextResolverRegistrationOptions = {},
56
+ ): Result<void, string> {
57
+ for (const r of resolvers) {
58
+ const registrationResult = this.register(r, options);
59
+ if (registrationResult.isErr()) {
60
+ return registrationResult;
61
+ }
62
+ }
63
+
64
+ return Result.ok(undefined);
65
+ }
66
+
67
+ get(path: string): ContextValueResolver | undefined {
68
+ return this.resolvers.get(path)?.resolver;
69
+ }
70
+
71
+ has(path: string): boolean {
72
+ return this.resolvers.has(path);
73
+ }
74
+
75
+ getNamespace(path: string): string | undefined {
76
+ return this.resolvers.get(path)?.namespace;
77
+ }
78
+ }
79
+
80
+ export function registerNamespacedContextResolversIn(
81
+ registry: ContextResolverRegistry,
82
+ namespace: string,
83
+ resolvers: readonly ContextValueResolver[],
84
+ ): Result<ContextResolverRegistry, string> {
85
+ const registrationResult = registry.registerAll(resolvers, {
86
+ namespace,
87
+ });
88
+ if (registrationResult.isErr()) {
89
+ return Result.err(registrationResult.errorOrNull()!);
90
+ }
91
+
92
+ return Result.ok(registry);
93
+ }
94
+
95
+ export { DEFAULT_CONTEXT_RESOLVER_NAMESPACE };
@@ -0,0 +1,31 @@
1
+ import { Result } from "../../result/result";
2
+
3
+ import type { ContextSeed } from "./context-seed";
4
+
5
+ export interface ContextResolverRetryOptions {
6
+ readonly maxAttempts?: number;
7
+ readonly initialDelayMs?: number;
8
+ readonly maxDelayMs?: number;
9
+ readonly backoffMultiplier?: number;
10
+ }
11
+
12
+ export interface ContextResolverCircuitBreakerOptions {
13
+ readonly failureThreshold?: number;
14
+ readonly cooldownMs?: number;
15
+ }
16
+
17
+ export interface ContextResolverResilienceOptions {
18
+ readonly timeoutMs?: number;
19
+ readonly retry?: ContextResolverRetryOptions | false;
20
+ readonly circuitBreaker?: ContextResolverCircuitBreakerOptions | false;
21
+ }
22
+
23
+ /**
24
+ * A resolver that knows how to produce a value for a specific context path.
25
+ * Designed as async to allow future DB/API lookups without breaking the interface.
26
+ */
27
+ export interface ContextValueResolver {
28
+ readonly path: string;
29
+ readonly resilience?: ContextResolverResilienceOptions;
30
+ resolve(seed: ContextSeed): Promise<Result<unknown, string>>;
31
+ }
@@ -0,0 +1,32 @@
1
+ import { Result } from "../../result/result";
2
+
3
+ import type { SchoolId, TenantId } from "../policy-ids";
4
+
5
+ /**
6
+ * Seed data provided by the use case / caller.
7
+ * Contains raw IDs and inputs that the context system will resolve into paths.
8
+ * Each resolver is responsible for validating and extracting what it needs from fields.
9
+ */
10
+ export interface ContextSeed {
11
+ readonly tenantId: TenantId;
12
+ readonly schoolId: SchoolId;
13
+ readonly fields: Record<string, unknown>;
14
+ }
15
+
16
+ export class ContextSeedValidator {
17
+ private static isBlankSeedId(value: string): boolean {
18
+ return value.trim().length === 0;
19
+ }
20
+
21
+ static validate(seed: ContextSeed): Result<ContextSeed, string> {
22
+ if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) {
23
+ return Result.err("ContextSeed tenantId must be a non-empty string");
24
+ }
25
+
26
+ if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) {
27
+ return Result.err("ContextSeed schoolId must be a non-empty string");
28
+ }
29
+
30
+ return Result.ok(seed);
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ export { PolicyContextBuilder } from "./context-builder";
2
+ export {
3
+ ContextResolverRegistry,
4
+ DEFAULT_CONTEXT_RESOLVER_NAMESPACE,
5
+ registerNamespacedContextResolversIn,
6
+ } from "./context-registry";
7
+ export {
8
+ contextResolverRegistry,
9
+ registerNamespacedContextResolvers,
10
+ } from "./context-registry.instance";
11
+ export type { ContextResolverRegistrationOptions } from "./context-registry";
12
+ export type {
13
+ ContextResolverCircuitBreakerOptions,
14
+ ContextResolverResilienceOptions,
15
+ ContextResolverRetryOptions,
16
+ ContextValueResolver,
17
+ } from "./context-resolver";
18
+ export type { PolicyContextBuilderOptions } from "./context-builder";
19
+ export { ContextSeedValidator } from "./context-seed";
20
+ export type { ContextSeed } from "./context-seed";
21
+ export { PolicyContextPath } from "./path";