@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,592 @@
1
+ import { PolicyContextPath } from "../../context";
2
+ import { PolicyDateUtils } from "../../utils";
3
+ import { Result } from "../../../result/result";
4
+
5
+ import type {
6
+ ConditionAndNode,
7
+ ConditionLeafNode,
8
+ ConditionNode,
9
+ ConditionNotNode,
10
+ ConditionOp,
11
+ ConditionOrNode,
12
+ } from "./condition-types";
13
+ import type {
14
+ ConditionEvaluationOptions,
15
+ ConditionEvaluationReport,
16
+ } from "../condition-evaluator-reporter";
17
+ import type { PolicyContext } from "../gate-types";
18
+
19
+ const CONDITION_EVAL_THROWN_TAG = "CONDITION_EVAL_THREW";
20
+ const NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG =
21
+ "NULLISH_NUMERIC_OPERAND_NOT_ALLOWED";
22
+ const NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG = "NULLISH_DATE_OPERAND_NOT_ALLOWED";
23
+ const INVALID_DATE_OPERAND_TAG = "INVALID_DATE_OPERAND";
24
+ const EMPTY_OR_CONDITION_TAG = "EMPTY_OR_CONDITION";
25
+ const ISO_8601_UTC_DATE_PATTERN =
26
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
27
+
28
+ export interface ConditionEvaluationTrace {
29
+ readonly conditionPath: string;
30
+ readonly decisiveNode: ConditionNode;
31
+ readonly lastEvaluatedLeaf: ConditionLeafNode;
32
+ readonly lastEvaluatedLeafPath: string;
33
+ readonly lastEvaluatedLeafResult: boolean;
34
+ }
35
+
36
+ export interface TracedConditionEvaluation {
37
+ readonly matched: boolean;
38
+ readonly trace: ConditionEvaluationTrace;
39
+ }
40
+
41
+ type RelationalOperator = Extract<ConditionOp, "gt" | "gte" | "lt" | "lte">;
42
+
43
+ export class ConditionEvaluatorV1 {
44
+ constructor(
45
+ private readonly context: PolicyContext,
46
+ private readonly options: ConditionEvaluationOptions,
47
+ ) {}
48
+
49
+ private static isLeafNode(node: ConditionNode): node is ConditionLeafNode {
50
+ return "field" in node && "op" in node;
51
+ }
52
+
53
+ private static isAndNode(node: ConditionNode): node is ConditionAndNode {
54
+ return "and" in node;
55
+ }
56
+
57
+ private static isOrNode(node: ConditionNode): node is ConditionOrNode {
58
+ return "or" in node;
59
+ }
60
+
61
+ private static isNotNode(node: ConditionNode): node is ConditionNotNode {
62
+ return "not" in node;
63
+ }
64
+
65
+ private static isRelationalOperator(
66
+ op: ConditionOp,
67
+ ): op is RelationalOperator {
68
+ return op === "gt" || op === "gte" || op === "lt" || op === "lte";
69
+ }
70
+
71
+ private static describeError(error: unknown): {
72
+ name: string;
73
+ message: string;
74
+ } {
75
+ if (error instanceof Error) {
76
+ return {
77
+ name: error.name,
78
+ message: error.message,
79
+ };
80
+ }
81
+
82
+ return {
83
+ name: "NonErrorThrown",
84
+ message: String(error),
85
+ };
86
+ }
87
+
88
+ private static buildNullishNumericOperandMessage(
89
+ node: ConditionLeafNode,
90
+ actual: null | undefined,
91
+ ): string {
92
+ const resolvedValue = actual === null ? "null" : "undefined";
93
+
94
+ return `${NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for numeric operator "${node.op}"`;
95
+ }
96
+
97
+ private static buildNullishDateOperandMessage(
98
+ node: ConditionLeafNode,
99
+ actual: null | undefined,
100
+ ): string {
101
+ const resolvedValue = actual === null ? "null" : "undefined";
102
+
103
+ return `${NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for date comparison operator "${node.op}"`;
104
+ }
105
+
106
+ private static buildInvalidDateOperandMessage(
107
+ node: ConditionLeafNode,
108
+ ): string {
109
+ return `${INVALID_DATE_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires Date or ISO 8601 UTC string operands`;
110
+ }
111
+
112
+ private static buildEmptyOrConditionMessage(): string {
113
+ return `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`;
114
+ }
115
+
116
+ private static parseComparableDate(
117
+ value: unknown,
118
+ ): Result<Date, string> | null {
119
+ if (value instanceof Date) {
120
+ if (!PolicyDateUtils.isValid(value)) {
121
+ return Result.err("Invalid Date instance");
122
+ }
123
+
124
+ return Result.ok(value);
125
+ }
126
+
127
+ if (typeof value !== "string" || !ISO_8601_UTC_DATE_PATTERN.test(value)) {
128
+ return null;
129
+ }
130
+
131
+ const parsed = new Date(value);
132
+ if (!PolicyDateUtils.isValid(parsed) || parsed.toISOString() !== value) {
133
+ return Result.err("Invalid ISO 8601 UTC date string");
134
+ }
135
+
136
+ return Result.ok(parsed);
137
+ }
138
+
139
+ private static evaluateRelationalNumbers(
140
+ actual: number,
141
+ op: RelationalOperator,
142
+ expected: number,
143
+ ): boolean {
144
+ switch (op) {
145
+ case "gt":
146
+ return actual > expected;
147
+ case "gte":
148
+ return actual >= expected;
149
+ case "lt":
150
+ return actual < expected;
151
+ case "lte":
152
+ return actual <= expected;
153
+ }
154
+ }
155
+
156
+ private buildReport(params: {
157
+ readonly level: ConditionEvaluationReport["level"];
158
+ readonly tag: ConditionEvaluationReport["tag"];
159
+ readonly message: string;
160
+ readonly details: Readonly<Record<string, unknown>>;
161
+ }): ConditionEvaluationReport {
162
+ return {
163
+ occurredAt: new Date(),
164
+ level: params.level,
165
+ tag: params.tag,
166
+ message: params.message,
167
+ engineVersion: this.options.engineVersion,
168
+ details: params.details,
169
+ };
170
+ }
171
+
172
+ private conditionEvalErr<TValue>(
173
+ node: ConditionNode,
174
+ error: unknown,
175
+ ): Result<TValue, string> {
176
+ const cause = ConditionEvaluatorV1.describeError(error);
177
+ const message = `${CONDITION_EVAL_THROWN_TAG}: ${cause.name}: ${cause.message}`;
178
+
179
+ this.options.reporter.error(
180
+ this.buildReport({
181
+ level: "error",
182
+ tag: CONDITION_EVAL_THROWN_TAG,
183
+ message,
184
+ details: { node, cause },
185
+ }),
186
+ );
187
+
188
+ return Result.err(message);
189
+ }
190
+
191
+ private reportDateOperandError(
192
+ node: ConditionLeafNode,
193
+ actual: unknown,
194
+ ): Result<boolean, string> {
195
+ const message = ConditionEvaluatorV1.buildInvalidDateOperandMessage(node);
196
+
197
+ this.options.reporter.error(
198
+ this.buildReport({
199
+ level: "error",
200
+ tag: INVALID_DATE_OPERAND_TAG,
201
+ message,
202
+ details: {
203
+ field: node.field,
204
+ op: node.op,
205
+ actual,
206
+ expected: node.value,
207
+ node,
208
+ },
209
+ }),
210
+ );
211
+
212
+ return Result.err(message);
213
+ }
214
+
215
+ private evaluateDateRelationalNode(
216
+ node: ConditionLeafNode,
217
+ actual: unknown,
218
+ ): Result<boolean, string> | null {
219
+ if (!ConditionEvaluatorV1.isRelationalOperator(node.op)) {
220
+ return null;
221
+ }
222
+
223
+ const actualDateResult = ConditionEvaluatorV1.parseComparableDate(actual);
224
+ const expectedDateResult = ConditionEvaluatorV1.parseComparableDate(
225
+ node.value,
226
+ );
227
+
228
+ if (actualDateResult === null && expectedDateResult === null) {
229
+ return null;
230
+ }
231
+
232
+ const allowsNull = node.allowNull === true;
233
+
234
+ if (actual === null) {
235
+ if (allowsNull) {
236
+ return Result.ok(false);
237
+ }
238
+
239
+ const message = ConditionEvaluatorV1.buildNullishDateOperandMessage(
240
+ node,
241
+ actual,
242
+ );
243
+ this.options.reporter.error(
244
+ this.buildReport({
245
+ level: "error",
246
+ tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
247
+ message,
248
+ details: {
249
+ field: node.field,
250
+ op: node.op,
251
+ actual,
252
+ expected: node.value,
253
+ allowNull: allowsNull,
254
+ node,
255
+ },
256
+ }),
257
+ );
258
+
259
+ return Result.err(message);
260
+ }
261
+
262
+ if (actual === undefined) {
263
+ const message = ConditionEvaluatorV1.buildNullishDateOperandMessage(
264
+ node,
265
+ actual,
266
+ );
267
+ this.options.reporter.error(
268
+ this.buildReport({
269
+ level: "error",
270
+ tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
271
+ message,
272
+ details: {
273
+ field: node.field,
274
+ op: node.op,
275
+ actual,
276
+ expected: node.value,
277
+ allowNull: allowsNull,
278
+ node,
279
+ },
280
+ }),
281
+ );
282
+
283
+ return Result.err(message);
284
+ }
285
+
286
+ if (
287
+ actualDateResult === null ||
288
+ actualDateResult.isErr() ||
289
+ expectedDateResult === null ||
290
+ expectedDateResult.isErr()
291
+ ) {
292
+ return this.reportDateOperandError(node, actual);
293
+ }
294
+
295
+ return Result.ok(
296
+ ConditionEvaluatorV1.evaluateRelationalNumbers(
297
+ actualDateResult.getOrNull()!.getTime(),
298
+ node.op,
299
+ expectedDateResult.getOrNull()!.getTime(),
300
+ ),
301
+ );
302
+ }
303
+
304
+ private evaluateOperator(
305
+ actual: unknown,
306
+ op: ConditionOp,
307
+ expected: unknown,
308
+ ): boolean {
309
+ switch (op) {
310
+ case "eq":
311
+ return actual === expected;
312
+ case "neq":
313
+ return actual !== expected;
314
+ case "gt":
315
+ return (
316
+ typeof actual === "number" &&
317
+ typeof expected === "number" &&
318
+ ConditionEvaluatorV1.evaluateRelationalNumbers(actual, op, expected)
319
+ );
320
+ case "gte":
321
+ return (
322
+ typeof actual === "number" &&
323
+ typeof expected === "number" &&
324
+ ConditionEvaluatorV1.evaluateRelationalNumbers(actual, op, expected)
325
+ );
326
+ case "lt":
327
+ return (
328
+ typeof actual === "number" &&
329
+ typeof expected === "number" &&
330
+ ConditionEvaluatorV1.evaluateRelationalNumbers(actual, op, expected)
331
+ );
332
+ case "lte":
333
+ return (
334
+ typeof actual === "number" &&
335
+ typeof expected === "number" &&
336
+ ConditionEvaluatorV1.evaluateRelationalNumbers(actual, op, expected)
337
+ );
338
+ case "in":
339
+ return Array.isArray(expected) && expected.includes(actual);
340
+ case "notIn":
341
+ return Array.isArray(expected) && !expected.includes(actual);
342
+ case "isNull":
343
+ return actual === null;
344
+ case "isNotNull":
345
+ return actual !== null && actual !== undefined;
346
+ }
347
+ }
348
+
349
+ private evaluateNumericRelationalNode(
350
+ node: ConditionLeafNode,
351
+ actual: unknown,
352
+ ): Result<boolean, string> {
353
+ const allowsNull = node.allowNull === true;
354
+ if (actual === undefined || (actual === null && !allowsNull)) {
355
+ const message = ConditionEvaluatorV1.buildNullishNumericOperandMessage(
356
+ node,
357
+ actual as null | undefined,
358
+ );
359
+
360
+ this.options.reporter.error(
361
+ this.buildReport({
362
+ level: "error",
363
+ tag: NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG,
364
+ message,
365
+ details: {
366
+ field: node.field,
367
+ op: node.op,
368
+ actual,
369
+ expected: node.value,
370
+ allowNull: allowsNull,
371
+ node,
372
+ },
373
+ }),
374
+ );
375
+
376
+ return Result.err(message);
377
+ }
378
+
379
+ return Result.ok(this.evaluateOperator(actual, node.op, node.value));
380
+ }
381
+
382
+ private evaluateLeafNode(node: ConditionLeafNode): Result<boolean, string> {
383
+ const actualResult = PolicyContextPath.getOrAbsent(
384
+ this.context,
385
+ node.field,
386
+ );
387
+ if (actualResult.isErr()) {
388
+ this.options.reporter.warn(
389
+ this.buildReport({
390
+ level: "warn",
391
+ tag: "MISSING_CONTEXT_FIELD",
392
+ message: `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
393
+ details: { field: node.field, node },
394
+ }),
395
+ );
396
+
397
+ return Result.err(
398
+ `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
399
+ );
400
+ }
401
+
402
+ try {
403
+ const actual = actualResult.getOrThrow();
404
+ if (ConditionEvaluatorV1.isRelationalOperator(node.op)) {
405
+ const dateResult = this.evaluateDateRelationalNode(node, actual);
406
+ if (dateResult !== null) {
407
+ return dateResult;
408
+ }
409
+
410
+ return this.evaluateNumericRelationalNode(node, actual);
411
+ }
412
+
413
+ return Result.ok(this.evaluateOperator(actual, node.op, node.value));
414
+ } catch (error) {
415
+ return this.conditionEvalErr(node, error);
416
+ }
417
+ }
418
+
419
+ private buildTrace(params: {
420
+ readonly conditionPath: string;
421
+ readonly decisiveNode: ConditionNode;
422
+ readonly lastEvaluatedLeaf: ConditionLeafNode;
423
+ readonly lastEvaluatedLeafPath: string;
424
+ readonly lastEvaluatedLeafResult: boolean;
425
+ }): ConditionEvaluationTrace {
426
+ return {
427
+ conditionPath: params.conditionPath,
428
+ decisiveNode: params.decisiveNode,
429
+ lastEvaluatedLeaf: params.lastEvaluatedLeaf,
430
+ lastEvaluatedLeafPath: params.lastEvaluatedLeafPath,
431
+ lastEvaluatedLeafResult: params.lastEvaluatedLeafResult,
432
+ };
433
+ }
434
+
435
+ private evaluateWithTraceInternal(
436
+ node: ConditionNode,
437
+ path: string,
438
+ ): Result<TracedConditionEvaluation, string> {
439
+ if (ConditionEvaluatorV1.isLeafNode(node)) {
440
+ const leafResult = this.evaluateLeafNode(node);
441
+ if (leafResult.isErr()) {
442
+ return Result.err(leafResult.errorOrNull()!);
443
+ }
444
+
445
+ const matched = leafResult.getOrNull()!;
446
+ return Result.ok({
447
+ matched,
448
+ trace: this.buildTrace({
449
+ conditionPath: path,
450
+ decisiveNode: node,
451
+ lastEvaluatedLeaf: node,
452
+ lastEvaluatedLeafPath: path,
453
+ lastEvaluatedLeafResult: matched,
454
+ }),
455
+ });
456
+ }
457
+
458
+ if (ConditionEvaluatorV1.isAndNode(node)) {
459
+ let lastTracedChild: TracedConditionEvaluation | null = null;
460
+
461
+ for (const [index, child] of node.and.entries()) {
462
+ const childPath = `${path}.and[${index}]`;
463
+ const childResult = this.evaluateWithTraceInternal(child, childPath);
464
+ if (childResult.isErr()) {
465
+ return Result.err(childResult.errorOrNull()!);
466
+ }
467
+
468
+ const tracedChild = childResult.getOrNull()!;
469
+ lastTracedChild = tracedChild;
470
+ if (!tracedChild.matched) {
471
+ return Result.ok({
472
+ matched: false,
473
+ trace: this.buildTrace({
474
+ conditionPath: childPath,
475
+ decisiveNode: child,
476
+ lastEvaluatedLeaf: tracedChild.trace.lastEvaluatedLeaf,
477
+ lastEvaluatedLeafPath: tracedChild.trace.lastEvaluatedLeafPath,
478
+ lastEvaluatedLeafResult:
479
+ tracedChild.trace.lastEvaluatedLeafResult,
480
+ }),
481
+ });
482
+ }
483
+ }
484
+
485
+ return Result.ok({
486
+ matched: true,
487
+ trace: lastTracedChild!.trace,
488
+ });
489
+ }
490
+
491
+ if (ConditionEvaluatorV1.isOrNode(node)) {
492
+ let lastTrace: ConditionEvaluationTrace | null = null;
493
+
494
+ for (const [index, child] of node.or.entries()) {
495
+ const childResult = this.evaluateWithTraceInternal(
496
+ child,
497
+ `${path}.or[${index}]`,
498
+ );
499
+ if (childResult.isErr()) {
500
+ return Result.err(childResult.errorOrNull()!);
501
+ }
502
+
503
+ const tracedChild = childResult.getOrNull()!;
504
+ lastTrace = tracedChild.trace;
505
+ if (tracedChild.matched) {
506
+ return Result.ok({
507
+ matched: true,
508
+ trace: tracedChild.trace,
509
+ });
510
+ }
511
+ }
512
+
513
+ if (lastTrace === null) {
514
+ return Result.err(ConditionEvaluatorV1.buildEmptyOrConditionMessage());
515
+ }
516
+
517
+ return Result.ok({ matched: false, trace: lastTrace! });
518
+ }
519
+
520
+ if (ConditionEvaluatorV1.isNotNode(node)) {
521
+ const childPath = `${path}.not`;
522
+ const childResult = this.evaluateWithTraceInternal(node.not, childPath);
523
+ if (childResult.isErr()) {
524
+ return Result.err(childResult.errorOrNull()!);
525
+ }
526
+
527
+ const tracedChild = childResult.getOrNull()!;
528
+ return Result.ok({
529
+ matched: !tracedChild.matched,
530
+ trace: this.buildTrace({
531
+ conditionPath: path,
532
+ decisiveNode: node,
533
+ lastEvaluatedLeaf: tracedChild.trace.lastEvaluatedLeaf,
534
+ lastEvaluatedLeafPath: tracedChild.trace.lastEvaluatedLeafPath,
535
+ lastEvaluatedLeafResult: tracedChild.trace.lastEvaluatedLeafResult,
536
+ }),
537
+ });
538
+ }
539
+
540
+ return this.conditionEvalErr(
541
+ node,
542
+ new TypeError("Invalid condition node shape"),
543
+ );
544
+ }
545
+
546
+ evaluate(node: ConditionNode): Result<boolean, string> {
547
+ try {
548
+ const result = this.evaluateWithTraceInternal(node, "$");
549
+ if (result.isErr()) {
550
+ return Result.err(result.errorOrNull()!);
551
+ }
552
+
553
+ return Result.ok(result.getOrNull()!.matched);
554
+ } catch (error) {
555
+ return this.conditionEvalErr(node, error);
556
+ }
557
+ }
558
+
559
+ evaluateWithTrace(
560
+ node: ConditionNode,
561
+ ): Result<TracedConditionEvaluation, string> {
562
+ try {
563
+ return this.evaluateWithTraceInternal(node, "$");
564
+ } catch (error) {
565
+ return this.conditionEvalErr(node, error);
566
+ }
567
+ }
568
+
569
+ static extractFields(node: ConditionNode): string[] {
570
+ if (ConditionEvaluatorV1.isLeafNode(node)) {
571
+ return [node.field];
572
+ }
573
+
574
+ if (ConditionEvaluatorV1.isAndNode(node)) {
575
+ return node.and.flatMap((child) =>
576
+ ConditionEvaluatorV1.extractFields(child),
577
+ );
578
+ }
579
+
580
+ if (ConditionEvaluatorV1.isOrNode(node)) {
581
+ return node.or.flatMap((child) =>
582
+ ConditionEvaluatorV1.extractFields(child),
583
+ );
584
+ }
585
+
586
+ if (ConditionEvaluatorV1.isNotNode(node)) {
587
+ return ConditionEvaluatorV1.extractFields(node.not);
588
+ }
589
+
590
+ return [];
591
+ }
592
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ import type { ConditionLeafNode, ConditionNode } from "./condition-types";
4
+
5
+ // ─── Shared v1 condition Zod schemas ─────────────────────────────────────────
6
+ // Extracted from gate/v1 so both gate/v1 and compute/v1 can validate
7
+ // condition nodes without either engine depending on the other.
8
+
9
+ const conditionOpSchema = z.enum([
10
+ "eq",
11
+ "neq",
12
+ "gt",
13
+ "gte",
14
+ "lt",
15
+ "lte",
16
+ "in",
17
+ "notIn",
18
+ "isNull",
19
+ "isNotNull",
20
+ ]);
21
+
22
+ export const conditionLeafNodeSchema: z.ZodType<ConditionLeafNode> = z
23
+ .object({
24
+ field: z.string().min(1),
25
+ op: conditionOpSchema,
26
+ value: z.unknown(),
27
+ allowNull: z.literal(true).optional(),
28
+ })
29
+ .strict() as z.ZodType<ConditionLeafNode>;
30
+
31
+ export const conditionNodeSchema: z.ZodType<ConditionNode> = z.lazy(() =>
32
+ z.union([
33
+ conditionLeafNodeSchema,
34
+ z
35
+ .object({
36
+ and: z.array(conditionNodeSchema).min(1),
37
+ })
38
+ .strict(),
39
+ z
40
+ .object({
41
+ or: z.array(conditionNodeSchema).min(1),
42
+ })
43
+ .strict(),
44
+ z
45
+ .object({
46
+ not: conditionNodeSchema,
47
+ })
48
+ .strict(),
49
+ ]),
50
+ );
@@ -0,0 +1,41 @@
1
+ // ─── Shared v1 condition DSL types ──────────────────────────────────────────
2
+ // These types belong to the v1 engine family: both gate/v1 and compute/v1
3
+ // share the same condition-tree DSL. Keeping them here avoids making compute/v1
4
+ // structurally dependent on gate/v1.
5
+
6
+ export type ConditionOp =
7
+ | "eq"
8
+ | "neq"
9
+ | "gt"
10
+ | "gte"
11
+ | "lt"
12
+ | "lte"
13
+ | "in"
14
+ | "notIn"
15
+ | "isNull"
16
+ | "isNotNull";
17
+
18
+ export interface ConditionLeafNode {
19
+ readonly field: string;
20
+ readonly op: ConditionOp;
21
+ readonly value: unknown;
22
+ readonly allowNull?: true;
23
+ }
24
+
25
+ export interface ConditionAndNode {
26
+ readonly and: readonly ConditionNode[];
27
+ }
28
+
29
+ export interface ConditionOrNode {
30
+ readonly or: readonly ConditionNode[];
31
+ }
32
+
33
+ export interface ConditionNotNode {
34
+ readonly not: ConditionNode;
35
+ }
36
+
37
+ export type ConditionNode =
38
+ | ConditionLeafNode
39
+ | ConditionAndNode
40
+ | ConditionOrNode
41
+ | ConditionNotNode;