@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,992 @@
1
+ import { a as stableStringify, i as sha256Hex, n as UnexpectedError, t as ValidationCode } from "./validation-code.js";
2
+ import { t as ValidationField } from "./validation-field.js";
3
+ import { _ as DomainException, f as SilentPolicyReporter, g as InvariantViolationException, h as isValidDate, l as Result, o as PolicyContextPath } from "./gate-v1-payload.schema.js";
4
+ //#region src/core/policies/utils/hash.ts
5
+ /**
6
+ * Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
7
+ * Object keys are sorted recursively so key order does not affect the hash.
8
+ * Array item order is preserved on purpose; callers that treat arrays as sets
9
+ * must normalize or sort them before hashing.
10
+ *
11
+ * Values that JSON.stringify would silently drop or coerce (undefined,
12
+ * non-finite numbers, functions, symbols, bigint) are rejected up-front to
13
+ * prevent semantically different payloads from collapsing to the same hash.
14
+ */
15
+ var PolicyHashing = class PolicyHashing {
16
+ static sha256(input) {
17
+ return sha256Hex(input);
18
+ }
19
+ static assertHashable(value, path) {
20
+ if (value === null) return;
21
+ const type = typeof value;
22
+ if (type === "undefined") throw new TypeError(`canonicalJson does not accept undefined values (at ${path})`);
23
+ if (type === "number" && !Number.isFinite(value)) throw new TypeError(`canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`);
24
+ if (type === "bigint" || type === "function" || type === "symbol") throw new TypeError(`canonicalJson does not accept ${type} values (at ${path})`);
25
+ if (type !== "object") return;
26
+ if (value instanceof Date) {
27
+ if (!isValidDate(value)) throw new TypeError(`canonicalJson does not accept Invalid Date (at ${path})`);
28
+ return;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ value.forEach((item, index) => {
32
+ PolicyHashing.assertHashable(item, `${path}[${index}]`);
33
+ });
34
+ return;
35
+ }
36
+ for (const [key, nested] of Object.entries(value)) PolicyHashing.assertHashable(nested, `${path}.${key}`);
37
+ }
38
+ static canonicalJson(value) {
39
+ PolicyHashing.assertHashable(value, "$");
40
+ return stableStringify(value);
41
+ }
42
+ static computePayloadHash(payload, policyKey, policyVersion) {
43
+ const canonical = PolicyHashing.canonicalJson({
44
+ policyKey,
45
+ policyVersion,
46
+ payload
47
+ });
48
+ return PolicyHashing.sha256(canonical);
49
+ }
50
+ };
51
+ //#endregion
52
+ //#region src/core/exceptions/validation-exception.ts
53
+ var ValidationException = class extends DomainException {
54
+ constructor(field, code, message) {
55
+ super(message);
56
+ this.field = field;
57
+ this.code = code;
58
+ }
59
+ };
60
+ var InvalidValueException = class extends ValidationException {
61
+ constructor(field, code, message) {
62
+ super(field, code, message);
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/core/policies/policy-ids.ts
67
+ const POLICY_DEFINITION_ID_FIELD = ValidationField.of("policyDefinitionId");
68
+ const POLICY_DECISION_ID_FIELD = ValidationField.of("policyDecisionId");
69
+ const TENANT_ID_FIELD = ValidationField.of("tenantId");
70
+ const SCHOOL_ID_FIELD = ValidationField.of("schoolId");
71
+ function assertIdString(value, field) {
72
+ if (typeof value !== "string") throw new InvalidValueException(field, ValidationCode.INVALID_FORMAT, `${field.value} must be a string, got ${typeof value}`);
73
+ if (value.trim().length === 0) throw new InvalidValueException(field, ValidationCode.BLANK, `${field.value} must be a non-empty string`);
74
+ return value;
75
+ }
76
+ function brandId(value, field) {
77
+ return assertIdString(value, field);
78
+ }
79
+ function asPolicyDefinitionId(value) {
80
+ return brandId(value, POLICY_DEFINITION_ID_FIELD);
81
+ }
82
+ function asPolicyDecisionId(value) {
83
+ return brandId(value, POLICY_DECISION_ID_FIELD);
84
+ }
85
+ function asTenantId(value) {
86
+ return brandId(value, TENANT_ID_FIELD);
87
+ }
88
+ function asSchoolId(value) {
89
+ return brandId(value, SCHOOL_ID_FIELD);
90
+ }
91
+ //#endregion
92
+ //#region src/core/policies/catalog/policy-catalog-entry.ts
93
+ /**
94
+ * Represents a single entry in the Policy Catalog.
95
+ * Encapsulates metadata and validation logic for a policy.
96
+ */
97
+ var PolicyCatalogEntry = class PolicyCatalogEntry {
98
+ constructor(props) {
99
+ this.key = props.key;
100
+ this.kind = props.kind;
101
+ this.gateEngineVersion = props.gateEngineVersion;
102
+ this.computeEngineVersion = props.computeEngineVersion;
103
+ this.payloadSchemaVersion = props.payloadSchemaVersion;
104
+ this.owner = props.owner;
105
+ this.allowedScopes = [...props.allowedScopes];
106
+ this.asOfSource = props.asOfSource;
107
+ this.contextRequirements = [...props.contextRequirements];
108
+ this.tags = [...props.tags ?? []];
109
+ this.description = props.description;
110
+ }
111
+ static assertFamilyConsistency(entries) {
112
+ if (entries.length === 0) throw new UnexpectedError("Cannot validate an empty PolicyCatalogEntry family");
113
+ const [first, ...rest] = entries;
114
+ for (const entry of rest) {
115
+ if (!first.key.equals(entry.key)) throw new UnexpectedError("Cannot merge PolicyCatalog entries with different keys");
116
+ if (first.kind !== entry.kind) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same kind`);
117
+ if (first.owner !== entry.owner) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same owner`);
118
+ if (first.asOfSource !== entry.asOfSource) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same asOfSource`);
119
+ if (first.description !== entry.description) throw new UnexpectedError(`PolicyCatalog entries for key "${first.key.toString()}" must share the same description`);
120
+ }
121
+ }
122
+ static from(entry) {
123
+ return entry instanceof PolicyCatalogEntry ? entry : new PolicyCatalogEntry(entry);
124
+ }
125
+ isGate() {
126
+ return this.kind === "GATE";
127
+ }
128
+ isCompute() {
129
+ return this.kind === "COMPUTE";
130
+ }
131
+ allowsScope(scope) {
132
+ return this.allowedScopes.includes(scope);
133
+ }
134
+ hasExplicitVersionSelector() {
135
+ return this.gateEngineVersion !== void 0 || this.computeEngineVersion !== void 0 || this.payloadSchemaVersion !== void 0;
136
+ }
137
+ matchesVersion(params) {
138
+ if (this.kind !== params.kind) return false;
139
+ if (this.gateEngineVersion !== void 0 && this.gateEngineVersion !== params.gateEngineVersion) return false;
140
+ if (this.computeEngineVersion !== void 0 && this.computeEngineVersion !== params.computeEngineVersion) return false;
141
+ if (this.payloadSchemaVersion !== void 0 && this.payloadSchemaVersion !== params.payloadSchemaVersion) return false;
142
+ return true;
143
+ }
144
+ toVariantKey() {
145
+ return [
146
+ this.key.toString(),
147
+ this.kind,
148
+ this.gateEngineVersion ?? "gate:any",
149
+ this.computeEngineVersion ?? "compute:any",
150
+ this.payloadSchemaVersion ?? "schema:any"
151
+ ].join("::");
152
+ }
153
+ usesNowAsOf() {
154
+ return this.asOfSource === "NOW";
155
+ }
156
+ usesCallerProvidedAsOf() {
157
+ return this.asOfSource === "CALLER_PROVIDED";
158
+ }
159
+ requiresContext(path) {
160
+ return this.contextRequirements.includes(path);
161
+ }
162
+ requiresAllContexts(paths) {
163
+ return paths.every((path) => this.requiresContext(path));
164
+ }
165
+ hasTag(tag) {
166
+ return this.tags.includes(tag);
167
+ }
168
+ equals(other) {
169
+ return this.key.equals(other.key);
170
+ }
171
+ toJSON() {
172
+ return {
173
+ key: this.key,
174
+ kind: this.kind,
175
+ ...this.gateEngineVersion !== void 0 ? { gateEngineVersion: this.gateEngineVersion } : {},
176
+ ...this.computeEngineVersion !== void 0 ? { computeEngineVersion: this.computeEngineVersion } : {},
177
+ ...this.payloadSchemaVersion !== void 0 ? { payloadSchemaVersion: this.payloadSchemaVersion } : {},
178
+ owner: this.owner,
179
+ allowedScopes: [...this.allowedScopes],
180
+ asOfSource: this.asOfSource,
181
+ contextRequirements: [...this.contextRequirements],
182
+ tags: this.tags.length > 0 ? [...this.tags] : void 0,
183
+ description: this.description
184
+ };
185
+ }
186
+ };
187
+ //#endregion
188
+ //#region src/core/policies/catalog/policy-catalog.ts
189
+ /**
190
+ * In-code catalog of all known policies.
191
+ * The catalog defines the universe of policies; the database cannot invent new ones.
192
+ */
193
+ var PolicyCatalog = class {
194
+ constructor(entries) {
195
+ const families = /* @__PURE__ */ new Map();
196
+ const versionedMap = /* @__PURE__ */ new Map();
197
+ for (const rawEntry of entries) {
198
+ const entry = PolicyCatalogEntry.from(rawEntry);
199
+ const key = entry.key.toString();
200
+ const variantKey = entry.toVariantKey();
201
+ if (versionedMap.has(variantKey)) throw new UnexpectedError(`Duplicate PolicyCatalog entry for key "${key}" and variant "${variantKey}"`);
202
+ const family = families.get(key) ?? [];
203
+ family.push(entry);
204
+ families.set(key, family);
205
+ versionedMap.set(variantKey, entry);
206
+ }
207
+ for (const family of families.values()) PolicyCatalogEntry.assertFamilyConsistency(family);
208
+ this.versionedEntries = versionedMap;
209
+ this.entriesByKey = families;
210
+ }
211
+ get(key) {
212
+ const familyResult = this.getFamily(key);
213
+ if (familyResult.isErr()) return Result.err(familyResult.errorOrNull());
214
+ const family = familyResult.getOrNull();
215
+ if (family.length > 1) return Result.err(`Policy key "${key}" has multiple catalog variants; use getVersioned(...) or getFamily(...)`);
216
+ const [entry] = family;
217
+ if (!entry) return Result.err(`Policy not found in catalog: "${key}"`);
218
+ return Result.ok(entry);
219
+ }
220
+ getFamily(key) {
221
+ const family = this.entriesByKey.get(key);
222
+ if (!family) return Result.err(`Policy not found in catalog: "${key}"`);
223
+ return Result.ok(family);
224
+ }
225
+ getVersioned(params) {
226
+ const family = this.entriesByKey.get(params.key);
227
+ if (!family || family.length === 0) return Result.err(`Policy not found in catalog: "${params.key}"`);
228
+ const exactMatch = family.find((entry) => entry.matchesVersion(params));
229
+ if (exactMatch) return Result.ok(exactMatch);
230
+ if (family.length === 1 && !family[0].hasExplicitVersionSelector()) return Result.ok(family[0]);
231
+ const versionDetails = [
232
+ params.kind,
233
+ params.gateEngineVersion !== void 0 ? `gateEngineVersion=${params.gateEngineVersion}` : null,
234
+ params.computeEngineVersion !== void 0 ? `computeEngineVersion=${params.computeEngineVersion}` : null,
235
+ params.payloadSchemaVersion !== void 0 ? `payloadSchemaVersion=${params.payloadSchemaVersion}` : null
236
+ ].filter((part) => part !== null).join(", ");
237
+ return Result.err(`Policy variant not found in catalog: "${params.key}" (${versionDetails})`);
238
+ }
239
+ list() {
240
+ return Array.from(this.versionedEntries.values());
241
+ }
242
+ has(key) {
243
+ return this.entriesByKey.has(key);
244
+ }
245
+ };
246
+ //#endregion
247
+ //#region src/core/policies/catalog/catalog.instance.ts
248
+ /**
249
+ * Aggregates catalog entries contributed by each module package into a single
250
+ * PolicyCatalog. The core knows nothing about concrete modules — callers
251
+ * are responsible for passing all relevant packages at composition time.
252
+ */
253
+ var PolicyCatalogFactory = class {
254
+ static build(packages) {
255
+ return new PolicyCatalog(packages.flatMap((policyPackage) => Array.from(policyPackage.catalogEntries).map(PolicyCatalogEntry.from)));
256
+ }
257
+ };
258
+ //#endregion
259
+ //#region src/core/policies/catalog/policy-key.ts
260
+ /**
261
+ * PolicyKey value object.
262
+ *
263
+ * Format: `module.area.policy_name` (exactly 3 dot-separated segments).
264
+ * - module: structural segment (lowercase alphanumeric + underscore, starting with a letter)
265
+ * - area: structural segment (lowercase alphanumeric + underscore, starting with a letter)
266
+ * - name: specific policy identifier (lowercase alphanumeric + underscore, starting with a letter)
267
+ */
268
+ const SEGMENT_PATTERN = /^[a-z][a-z0-9_]*$/;
269
+ const MAX_POLICY_KEY_LENGTH = 255;
270
+ var PolicyKey = class PolicyKey {
271
+ constructor(module, area, name) {
272
+ this.module = module;
273
+ this.area = area;
274
+ this.name = name;
275
+ Object.freeze(this);
276
+ }
277
+ static parse(raw) {
278
+ if (!raw || typeof raw !== "string") return Result.err("PolicyKey cannot be empty");
279
+ if (raw.length > MAX_POLICY_KEY_LENGTH) return Result.err(`PolicyKey cannot exceed ${MAX_POLICY_KEY_LENGTH} characters, got ${raw.length}`);
280
+ const segments = raw.split(".");
281
+ if (segments.length !== 3) return Result.err(`PolicyKey must have exactly 3 segments (module.area.name), got ${segments.length}: "${raw}"`);
282
+ const [rawModule, rawArea, name] = segments;
283
+ if (!SEGMENT_PATTERN.test(rawModule)) return Result.err(`PolicyKey segment "module" is invalid: "${rawModule}". Must start with a lowercase letter and contain only [a-z0-9_].`);
284
+ if (!SEGMENT_PATTERN.test(rawArea)) return Result.err(`PolicyKey segment "area" is invalid: "${rawArea}". Must start with a lowercase letter and contain only [a-z0-9_].`);
285
+ if (!SEGMENT_PATTERN.test(name)) return Result.err(`PolicyKey segment "name" is invalid: "${name}". Must start with a lowercase letter and contain only [a-z0-9_].`);
286
+ return Result.ok(new PolicyKey(rawModule, rawArea, name));
287
+ }
288
+ toString() {
289
+ return `${this.module}.${this.area}.${this.name}`;
290
+ }
291
+ equals(other) {
292
+ return this.toString() === other.toString();
293
+ }
294
+ };
295
+ //#endregion
296
+ //#region src/core/policies/defs/policy-scope.ts
297
+ /**
298
+ * Numeric weight for scope specificity (higher = more specific).
299
+ */
300
+ const SCOPE_WEIGHT = {
301
+ SCHOOL: 3,
302
+ TENANT: 2,
303
+ GLOBAL: 1
304
+ };
305
+ var PolicyScopeMatcher = class PolicyScopeMatcher {
306
+ static weight(level) {
307
+ return SCOPE_WEIGHT[level];
308
+ }
309
+ static matchesChain(defScope, chain) {
310
+ return chain.some((scope) => PolicyScopeMatcher.equals(defScope, scope));
311
+ }
312
+ static equals(a, b) {
313
+ return a.level === b.level && a.tenantId === b.tenantId && a.schoolId === b.schoolId;
314
+ }
315
+ };
316
+ //#endregion
317
+ //#region src/core/policies/defs/in-memory-policy-definition-repo.ts
318
+ var InMemoryPolicyDefinitionRepository = class {
319
+ constructor(definitions) {
320
+ this.definitions = definitions;
321
+ this.definitionsByPolicyKey = definitions.reduce((index, definition) => {
322
+ const existingDefinitions = index.get(definition.policyKey) ?? [];
323
+ existingDefinitions.push(definition);
324
+ index.set(definition.policyKey, existingDefinitions);
325
+ return index;
326
+ }, /* @__PURE__ */ new Map());
327
+ }
328
+ findCandidates(params) {
329
+ const { policyKey, kind, payloadSchemaVersion, asOf, contextVersion, scopeChain } = params;
330
+ return (this.definitionsByPolicyKey.get(policyKey) ?? []).filter((def) => {
331
+ if (def.kind !== kind) return false;
332
+ if (payloadSchemaVersion !== void 0 && def.payloadSchemaVersion !== payloadSchemaVersion) return false;
333
+ if (!def.enabled) return false;
334
+ if (def.status !== "PUBLISHED") return false;
335
+ if (def.effectiveFrom > asOf) return false;
336
+ if (def.effectiveTo !== null && def.effectiveTo <= asOf) return false;
337
+ if (contextVersion < def.contextVersionMin) return false;
338
+ if (contextVersion > def.contextVersionMax) return false;
339
+ if (!PolicyScopeMatcher.matchesChain(def.scope, scopeChain)) return false;
340
+ return true;
341
+ });
342
+ }
343
+ };
344
+ //#endregion
345
+ //#region src/core/policies/defs/policy-semver.ts
346
+ const POLICY_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
347
+ function parseSemver(version) {
348
+ const [core, preRelease = null] = version.split("+")[0].split("-");
349
+ const [major, minor, patch] = core.split(".").map(Number);
350
+ return {
351
+ major,
352
+ minor,
353
+ patch,
354
+ preRelease: preRelease ?? null
355
+ };
356
+ }
357
+ function comparePreRelease(a, b) {
358
+ const aIds = a.split(".");
359
+ const bIds = b.split(".");
360
+ const len = Math.max(aIds.length, bIds.length);
361
+ for (let i = 0; i < len; i++) {
362
+ const ai = aIds[i];
363
+ const bi = bIds[i];
364
+ if (ai === void 0) return -1;
365
+ if (bi === void 0) return 1;
366
+ const aNum = Number(ai);
367
+ const bNum = Number(bi);
368
+ const aIsNum = !Number.isNaN(aNum);
369
+ const bIsNum = !Number.isNaN(bNum);
370
+ if (aIsNum && !bIsNum) return -1;
371
+ if (!aIsNum && bIsNum) return 1;
372
+ if (aIsNum && bIsNum) {
373
+ if (aNum !== bNum) return aNum - bNum;
374
+ } else {
375
+ if (ai < bi) return -1;
376
+ if (ai > bi) return 1;
377
+ }
378
+ }
379
+ return 0;
380
+ }
381
+ /**
382
+ * Compares two valid semver strings.
383
+ * Returns a negative number if a < b, 0 if a === b, positive if a > b.
384
+ *
385
+ * Callers are expected to pass only strings that already passed
386
+ * `POLICY_SEMVER_PATTERN` validation (i.e. values stored in PolicyDefinition).
387
+ */
388
+ function comparePolicySemver(a, b) {
389
+ const av = parseSemver(a);
390
+ const bv = parseSemver(b);
391
+ if (av.major !== bv.major) return av.major - bv.major;
392
+ if (av.minor !== bv.minor) return av.minor - bv.minor;
393
+ if (av.patch !== bv.patch) return av.patch - bv.patch;
394
+ if (av.preRelease === null && bv.preRelease !== null) return 1;
395
+ if (av.preRelease !== null && bv.preRelease === null) return -1;
396
+ if (av.preRelease !== null && bv.preRelease !== null) return comparePreRelease(av.preRelease, bv.preRelease);
397
+ return 0;
398
+ }
399
+ //#endregion
400
+ //#region src/core/policies/defs/policy-definition.ts
401
+ var PolicyDefinition = class PolicyDefinition {
402
+ static assertPolicyVersionIsSemver(policyVersion) {
403
+ if (!POLICY_SEMVER_PATTERN.test(policyVersion)) throw new InvariantViolationException(`PolicyDefinition.policyVersion must be a valid semver. Received: "${policyVersion}"`);
404
+ }
405
+ constructor(props) {
406
+ this.id = props.id;
407
+ this.policyKey = props.policyKey;
408
+ PolicyDefinition.assertPolicyVersionIsSemver(props.policyVersion);
409
+ this.policyVersion = props.policyVersion;
410
+ this.payloadSchemaVersion = props.payloadSchemaVersion;
411
+ this.contextVersionMin = props.contextVersionMin;
412
+ this.contextVersionMax = props.contextVersionMax;
413
+ this.enabled = props.enabled;
414
+ this.status = props.status;
415
+ this.scope = props.scope;
416
+ this.effectiveFrom = props.effectiveFrom;
417
+ this.effectiveTo = props.effectiveTo;
418
+ this.priority = props.priority;
419
+ this.payloadJson = props.payloadJson;
420
+ this.payloadHash = props.payloadHash;
421
+ this.createdAt = props.createdAt;
422
+ this.publishedAt = props.publishedAt;
423
+ this.kind = props.kind;
424
+ if (props.kind === "GATE") {
425
+ if (props.gateEngineVersion === void 0) throw new InvariantViolationException(`PolicyDefinition of kind 'GATE' must have a 'gateEngineVersion'. Policy key: ${props.policyKey}`);
426
+ this.gateEngineVersion = props.gateEngineVersion;
427
+ } else {
428
+ if (props.computeEngineVersion === void 0) throw new InvariantViolationException(`PolicyDefinition of kind 'COMPUTE' must have a 'computeEngineVersion'. Policy key: ${props.policyKey}`);
429
+ this.computeEngineVersion = props.computeEngineVersion;
430
+ }
431
+ }
432
+ static gate(props) {
433
+ return new PolicyDefinition({
434
+ ...props,
435
+ enabled: props.enabled ?? true,
436
+ kind: "GATE"
437
+ });
438
+ }
439
+ static compute(props) {
440
+ return new PolicyDefinition({
441
+ ...props,
442
+ enabled: props.enabled ?? true,
443
+ kind: "COMPUTE"
444
+ });
445
+ }
446
+ isGate() {
447
+ return this.kind === "GATE";
448
+ }
449
+ isCompute() {
450
+ return this.kind === "COMPUTE";
451
+ }
452
+ };
453
+ //#endregion
454
+ //#region src/core/policies/context/context-builder.ts
455
+ const DEFAULT_RESOLVER_RETRY_OPTIONS = {
456
+ maxAttempts: 1,
457
+ initialDelayMs: 25,
458
+ maxDelayMs: 1e3,
459
+ backoffMultiplier: 2
460
+ };
461
+ /**
462
+ * Builds a context object by resolving required paths from a seed.
463
+ */
464
+ var PolicyContextBuilder = class PolicyContextBuilder {
465
+ constructor(registry, options = {}) {
466
+ this.registry = registry;
467
+ this.circuitStates = /* @__PURE__ */ new Map();
468
+ this.defaultResolverResilience = options.defaultResolverResilience;
469
+ this.now = options.now ?? Date.now;
470
+ this.sleep = options.sleep ?? (async (delayMs) => {
471
+ if (delayMs <= 0) return;
472
+ await new Promise((resolve) => {
473
+ setTimeout(resolve, delayMs);
474
+ });
475
+ });
476
+ }
477
+ static clampPositiveInteger(value, fallback) {
478
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
479
+ return Math.max(1, Math.trunc(value));
480
+ }
481
+ static clampNonNegativeInteger(value, fallback) {
482
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
483
+ return Math.max(0, Math.trunc(value));
484
+ }
485
+ static normalizeTimeoutMs(value) {
486
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return null;
487
+ return Math.trunc(value);
488
+ }
489
+ static mergeRetryOptions(defaults, overrides) {
490
+ if (overrides === false) return false;
491
+ if (overrides !== void 0) return {
492
+ ...defaults === false || defaults === void 0 ? {} : defaults,
493
+ ...overrides
494
+ };
495
+ return defaults;
496
+ }
497
+ static mergeCircuitBreakerOptions(defaults, overrides) {
498
+ if (overrides === false) return false;
499
+ if (overrides !== void 0) return {
500
+ ...defaults === false || defaults === void 0 ? {} : defaults,
501
+ ...overrides
502
+ };
503
+ return defaults;
504
+ }
505
+ resolveResilienceOptions(resolver) {
506
+ const defaults = this.defaultResolverResilience;
507
+ const override = resolver.resilience;
508
+ const retry = PolicyContextBuilder.mergeRetryOptions(defaults?.retry, override?.retry);
509
+ const circuitBreaker = PolicyContextBuilder.mergeCircuitBreakerOptions(defaults?.circuitBreaker, override?.circuitBreaker);
510
+ const retryOptions = retry === false ? void 0 : retry;
511
+ const circuitBreakerOptions = circuitBreaker === false ? void 0 : circuitBreaker;
512
+ return {
513
+ timeoutMs: PolicyContextBuilder.normalizeTimeoutMs(override?.timeoutMs ?? defaults?.timeoutMs),
514
+ retry: {
515
+ maxAttempts: PolicyContextBuilder.clampPositiveInteger(retryOptions?.maxAttempts, DEFAULT_RESOLVER_RETRY_OPTIONS.maxAttempts),
516
+ initialDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.initialDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.initialDelayMs),
517
+ maxDelayMs: PolicyContextBuilder.clampNonNegativeInteger(retryOptions?.maxDelayMs, DEFAULT_RESOLVER_RETRY_OPTIONS.maxDelayMs),
518
+ backoffMultiplier: PolicyContextBuilder.clampPositiveInteger(retryOptions?.backoffMultiplier, DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier)
519
+ },
520
+ circuitBreaker: circuitBreakerOptions === void 0 ? null : {
521
+ failureThreshold: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.failureThreshold, 5),
522
+ cooldownMs: PolicyContextBuilder.clampPositiveInteger(circuitBreakerOptions.cooldownMs, 3e4)
523
+ }
524
+ };
525
+ }
526
+ getRetryDelayMs(attempt, retry) {
527
+ return Math.min(retry.initialDelayMs * retry.backoffMultiplier ** (attempt - 1), retry.maxDelayMs);
528
+ }
529
+ resetCircuit(path) {
530
+ this.circuitStates.delete(path);
531
+ }
532
+ isCircuitOpen(path, circuitBreaker) {
533
+ if (circuitBreaker === null) return false;
534
+ const state = this.circuitStates.get(path);
535
+ if (state === void 0 || state.openUntilMs === null) return false;
536
+ if (state.openUntilMs <= this.now()) {
537
+ this.resetCircuit(path);
538
+ return false;
539
+ }
540
+ return true;
541
+ }
542
+ recordResolverFailure(path, circuitBreaker) {
543
+ if (circuitBreaker === null) return;
544
+ const nextFailureCount = (this.circuitStates.get(path)?.failureCount ?? 0) + 1;
545
+ if (nextFailureCount >= circuitBreaker.failureThreshold) {
546
+ this.circuitStates.set(path, {
547
+ failureCount: 0,
548
+ openUntilMs: this.now() + circuitBreaker.cooldownMs
549
+ });
550
+ return;
551
+ }
552
+ this.circuitStates.set(path, {
553
+ failureCount: nextFailureCount,
554
+ openUntilMs: null
555
+ });
556
+ }
557
+ static describeThrownResolverError(error) {
558
+ if (error instanceof Error) return `threw ${error.name}: ${error.message}`;
559
+ return `threw ${String(error)}`;
560
+ }
561
+ async resolveOnce(resolver, seed, timeoutMs) {
562
+ const execute = async () => {
563
+ try {
564
+ return await resolver.resolve(seed);
565
+ } catch (error) {
566
+ return Result.err(PolicyContextBuilder.describeThrownResolverError(error));
567
+ }
568
+ };
569
+ if (timeoutMs === null) return execute();
570
+ let timeoutId;
571
+ try {
572
+ return await Promise.race([execute(), new Promise((resolve) => {
573
+ timeoutId = setTimeout(() => {
574
+ resolve(Result.err(`timed out after ${timeoutMs}ms`));
575
+ }, timeoutMs);
576
+ })]);
577
+ } finally {
578
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
579
+ }
580
+ }
581
+ async resolveWithResilience(path, resolver, seed) {
582
+ const resilience = this.resolveResilienceOptions(resolver);
583
+ if (this.isCircuitOpen(path, resilience.circuitBreaker)) return Result.err("circuit breaker is open");
584
+ let lastError = "resolver failed";
585
+ for (let attempt = 1; attempt <= resilience.retry.maxAttempts; attempt++) {
586
+ const result = await this.resolveOnce(resolver, seed, resilience.timeoutMs);
587
+ if (result.isOk()) {
588
+ this.resetCircuit(path);
589
+ return result;
590
+ }
591
+ lastError = result.errorOrNull() ?? lastError;
592
+ if (attempt < resilience.retry.maxAttempts) await this.sleep(this.getRetryDelayMs(attempt, resilience.retry));
593
+ }
594
+ this.recordResolverFailure(path, resilience.circuitBreaker);
595
+ return Result.err(lastError);
596
+ }
597
+ /**
598
+ * Resolves all required context paths and returns a flat context object.
599
+ * If any required path cannot be resolved, returns an error.
600
+ */
601
+ async build(requirements, seed) {
602
+ const context = {};
603
+ for (const path of requirements) {
604
+ const resolver = this.registry.get(path);
605
+ if (!resolver) return Result.err(`Missing resolver for path "${path}"`);
606
+ const result = await this.resolveWithResilience(path, resolver, seed);
607
+ if (result.isErr()) return Result.err(`Resolver error for path "${path}": ${result.errorOrNull()}`);
608
+ const setPathResult = PolicyContextPath.set(context, path, result.getOrNull());
609
+ if (setPathResult.isErr()) return Result.err(setPathResult.errorOrNull());
610
+ }
611
+ const missing = requirements.filter((path) => {
612
+ const result = PolicyContextPath.getOrAbsent(context, path);
613
+ return result.isErr() || result.getOrNull() === void 0;
614
+ });
615
+ if (missing.length > 0) return Result.err(`Context missing required paths after resolution: ${missing.join(", ")}`);
616
+ return Result.ok(context);
617
+ }
618
+ };
619
+ //#endregion
620
+ //#region src/core/policies/context/context-registry.ts
621
+ const DEFAULT_CONTEXT_RESOLVER_NAMESPACE = "default";
622
+ function normalizeNamespace(namespace) {
623
+ const normalized = namespace?.trim();
624
+ if (normalized === void 0 || normalized.length === 0) return DEFAULT_CONTEXT_RESOLVER_NAMESPACE;
625
+ return normalized;
626
+ }
627
+ /**
628
+ * Registry of context value resolvers, keyed by path.
629
+ */
630
+ var ContextResolverRegistry = class {
631
+ constructor() {
632
+ this.resolvers = /* @__PURE__ */ new Map();
633
+ }
634
+ register(resolver, options = {}) {
635
+ const namespace = normalizeNamespace(options.namespace);
636
+ const existing = this.resolvers.get(resolver.path);
637
+ if (existing !== void 0 && existing.namespace !== namespace) return Result.err(`Resolver path "${resolver.path}" is already registered by namespace "${existing.namespace}" and cannot be replaced by "${namespace}".`);
638
+ this.resolvers.set(resolver.path, {
639
+ namespace,
640
+ resolver
641
+ });
642
+ return Result.ok(void 0);
643
+ }
644
+ registerAll(resolvers, options = {}) {
645
+ for (const r of resolvers) {
646
+ const registrationResult = this.register(r, options);
647
+ if (registrationResult.isErr()) return registrationResult;
648
+ }
649
+ return Result.ok(void 0);
650
+ }
651
+ get(path) {
652
+ return this.resolvers.get(path)?.resolver;
653
+ }
654
+ has(path) {
655
+ return this.resolvers.has(path);
656
+ }
657
+ getNamespace(path) {
658
+ return this.resolvers.get(path)?.namespace;
659
+ }
660
+ };
661
+ function registerNamespacedContextResolversIn(registry, namespace, resolvers) {
662
+ const registrationResult = registry.registerAll(resolvers, { namespace });
663
+ if (registrationResult.isErr()) return Result.err(registrationResult.errorOrNull());
664
+ return Result.ok(registry);
665
+ }
666
+ //#endregion
667
+ //#region src/core/policies/context/context-registry.instance.ts
668
+ /**
669
+ * Official instance of the ContextResolverRegistry.
670
+ * Use `new ContextResolverRegistry()` plus `registerNamespacedContextResolversIn`
671
+ * when each runtime/composition root needs its own isolated resolver graph.
672
+ */
673
+ const contextResolverRegistry = new ContextResolverRegistry();
674
+ function registerNamespacedContextResolvers(namespace, resolvers) {
675
+ return registerNamespacedContextResolversIn(contextResolverRegistry, namespace, resolvers);
676
+ }
677
+ //#endregion
678
+ //#region src/core/policies/context/context-seed.ts
679
+ var ContextSeedValidator = class ContextSeedValidator {
680
+ static isBlankSeedId(value) {
681
+ return value.trim().length === 0;
682
+ }
683
+ static validate(seed) {
684
+ if (ContextSeedValidator.isBlankSeedId(seed.tenantId)) return Result.err("ContextSeed tenantId must be a non-empty string");
685
+ if (ContextSeedValidator.isBlankSeedId(seed.schoolId)) return Result.err("ContextSeed schoolId must be a non-empty string");
686
+ return Result.ok(seed);
687
+ }
688
+ };
689
+ //#endregion
690
+ //#region src/core/policies/asof/asof.ts
691
+ const DEFAULT_MAX_AS_OF_FUTURE_YEARS = 10;
692
+ var PolicyAsOfResolver = class PolicyAsOfResolver {
693
+ static resolveMaxFutureYears(options) {
694
+ const maxFutureYears = options.maxFutureYears ?? DEFAULT_MAX_AS_OF_FUTURE_YEARS;
695
+ if (!Number.isInteger(maxFutureYears) || maxFutureYears < 0) return Result.err(`deriveAsOf maxFutureYears must be a non-negative integer. Received: ${maxFutureYears}`);
696
+ return Result.ok(maxFutureYears);
697
+ }
698
+ static derive(source, seed, now, options = {}) {
699
+ const maxFutureYearsResult = PolicyAsOfResolver.resolveMaxFutureYears(options);
700
+ if (maxFutureYearsResult.isErr()) return Result.err(maxFutureYearsResult.errorOrNull());
701
+ const maxFutureYears = maxFutureYearsResult.getOrNull();
702
+ switch (source) {
703
+ case "NOW": return Result.ok(now);
704
+ case "CALLER_PROVIDED": {
705
+ const asOf = seed.fields["asOf"];
706
+ if (!isValidDate(asOf)) return Result.err("asOfSource \"CALLER_PROVIDED\" requires seed.fields.asOf to be a Date");
707
+ const maxFuture = new Date(now);
708
+ maxFuture.setFullYear(maxFuture.getFullYear() + maxFutureYears);
709
+ if (asOf > maxFuture) return Result.err(`asOf date is more than ${maxFutureYears} years in the future (received ${asOf.toISOString()})`);
710
+ return Result.ok(asOf);
711
+ }
712
+ default: return Result.err(`Unknown asOfSource: "${source}"`);
713
+ }
714
+ }
715
+ };
716
+ //#endregion
717
+ //#region src/core/policies/resolver/policy-resolver.ts
718
+ /**
719
+ * Selects the best policy definition from a list of already-filtered candidates.
720
+ *
721
+ * Ordering criteria (highest priority first):
722
+ * 1. Scope specificity: SCHOOL (3) > TENANT (2) > GLOBAL (1)
723
+ * 2. Priority: higher number wins
724
+ * 3. publishedAt descending (fallback to createdAt)
725
+ * 4. Engine version descending (prefer-latest-engine-version — deterministic tiebreaker)
726
+ * 5. policyVersion descending (semver — final deterministic tiebreaker)
727
+ */
728
+ var PolicyResolver = class {
729
+ resolveBest(candidates) {
730
+ if (candidates.length === 0) return Result.err("No matching policy definition found (candidates list is empty)");
731
+ const sorted = [...candidates].sort((a, b) => {
732
+ const scopeDiff = PolicyScopeMatcher.weight(b.scope.level) - PolicyScopeMatcher.weight(a.scope.level);
733
+ if (scopeDiff !== 0) return scopeDiff;
734
+ const priorityDiff = b.priority - a.priority;
735
+ if (priorityDiff !== 0) return priorityDiff;
736
+ const aDate = a.publishedAt ?? a.createdAt;
737
+ const dateDiff = (b.publishedAt ?? b.createdAt).getTime() - aDate.getTime();
738
+ if (dateDiff !== 0) return dateDiff;
739
+ const aEngine = a.gateEngineVersion ?? a.computeEngineVersion ?? 0;
740
+ const engineDiff = (b.gateEngineVersion ?? b.computeEngineVersion ?? 0) - aEngine;
741
+ if (engineDiff !== 0) return engineDiff;
742
+ return comparePolicySemver(b.policyVersion, a.policyVersion);
743
+ });
744
+ return Result.ok(sorted[0]);
745
+ }
746
+ };
747
+ //#endregion
748
+ //#region src/core/policies/service/policy-evaluation-error.ts
749
+ var PolicyEvaluationErrors = class {
750
+ static invalidContext(stage, policyKey, cause) {
751
+ return {
752
+ kind: "INVALID_CONTEXT",
753
+ stage,
754
+ policyKey,
755
+ message: "Policy evaluation context is invalid",
756
+ cause
757
+ };
758
+ }
759
+ static invalidPolicyKey(rawPolicyKey, cause) {
760
+ return {
761
+ kind: "INVALID_POLICY_KEY",
762
+ rawPolicyKey,
763
+ message: "Policy key is invalid",
764
+ cause
765
+ };
766
+ }
767
+ static policyNotFound(policyKey, cause) {
768
+ return {
769
+ kind: "POLICY_NOT_FOUND",
770
+ policyKey,
771
+ message: "Policy is not registered in the catalog",
772
+ cause
773
+ };
774
+ }
775
+ static policyDefinitionNotFound(policyKey, asOf, contextVersion, cause) {
776
+ return {
777
+ kind: "POLICY_DEFINITION_NOT_FOUND",
778
+ policyKey,
779
+ contextVersion,
780
+ asOf,
781
+ message: "No published policy definition matched the evaluation inputs",
782
+ cause
783
+ };
784
+ }
785
+ static policyVariantNotFound(params) {
786
+ return {
787
+ kind: "POLICY_VARIANT_NOT_FOUND",
788
+ policyKey: params.policyKey,
789
+ policyKind: params.policyKind,
790
+ payloadSchemaVersion: params.payloadSchemaVersion,
791
+ ...params.gateEngineVersion !== void 0 ? { gateEngineVersion: params.gateEngineVersion } : {},
792
+ ...params.computeEngineVersion !== void 0 ? { computeEngineVersion: params.computeEngineVersion } : {},
793
+ message: "No catalog variant matched the selected policy definition",
794
+ cause: params.cause
795
+ };
796
+ }
797
+ static engineFailure(policyKey, engine, engineVersion, cause) {
798
+ return {
799
+ kind: "ENGINE_FAILURE",
800
+ policyKey,
801
+ engine,
802
+ engineVersion,
803
+ message: "Policy engine evaluation failed",
804
+ cause
805
+ };
806
+ }
807
+ };
808
+ //#endregion
809
+ //#region src/core/policies/service/policy-service.ts
810
+ var PolicyService = class {
811
+ #reporter;
812
+ constructor(params) {
813
+ this.catalog = params.catalog;
814
+ this.contextBuilder = params.contextBuilder;
815
+ this.defRepo = params.defRepo;
816
+ this.resolver = params.resolver;
817
+ this.gateEngines = params.gateEngines;
818
+ this.computeRegistry = params.computeRegistry;
819
+ this.options = params.options ?? {};
820
+ this.#reporter = this.options.reporter ?? new SilentPolicyReporter();
821
+ }
822
+ #reportSafely(event) {
823
+ try {
824
+ this.#reporter.report(event);
825
+ } catch {}
826
+ }
827
+ resolveEvaluationNow(seed) {
828
+ const candidate = seed.fields["now"];
829
+ if (candidate === void 0) return Result.ok(/* @__PURE__ */ new Date());
830
+ if (!isValidDate(candidate)) return Result.err("seed.fields.now must be a valid Date when provided");
831
+ return Result.ok(candidate);
832
+ }
833
+ async evaluate(input) {
834
+ const result = await this.#evaluate(input);
835
+ if (result.isErr()) {
836
+ const error = result.errorOrNull();
837
+ this.#reportSafely({
838
+ kind: "policy-evaluation-failed",
839
+ level: "error",
840
+ policyKey: "policyKey" in error ? error.policyKey : void 0,
841
+ errorKind: error.kind,
842
+ message: error.message,
843
+ cause: error.cause,
844
+ occurredAt: /* @__PURE__ */ new Date()
845
+ });
846
+ } else {
847
+ const ok = result.getOrNull();
848
+ this.#reportSafely({
849
+ kind: "policy-evaluation-completed",
850
+ level: "info",
851
+ policyKey: ok.ref.policyKey,
852
+ decisionId: ok.decisionId,
853
+ engineKind: ok.kind,
854
+ occurredAt: ok.evaluatedAt
855
+ });
856
+ }
857
+ return result;
858
+ }
859
+ async #evaluate(input) {
860
+ const seedResult = ContextSeedValidator.validate(input.seed);
861
+ if (seedResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("SEED_VALIDATION", input.policyKey, seedResult.errorOrNull()));
862
+ const seed = seedResult.getOrNull();
863
+ const catalogFamilyResult = this.#resolveCatalogFamily(input.policyKey);
864
+ if (catalogFamilyResult.isErr()) return Result.err(catalogFamilyResult.errorOrNull());
865
+ const { key: policyKey, family } = catalogFamilyResult.getOrNull();
866
+ const familyEntry = family[0];
867
+ const nowResult = this.resolveEvaluationNow(seed);
868
+ if (nowResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("AS_OF_DERIVATION", policyKey, nowResult.errorOrNull()));
869
+ const now = nowResult.getOrNull();
870
+ const asOfResult = PolicyAsOfResolver.derive(familyEntry.asOfSource, seed, now, this.options.asOf);
871
+ if (asOfResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("AS_OF_DERIVATION", policyKey, asOfResult.errorOrNull()));
872
+ const asOf = asOfResult.getOrNull();
873
+ const definitionResult = this.#findCandidates(policyKey, familyEntry.kind, asOf, input);
874
+ if (definitionResult.isErr()) return Result.err(definitionResult.errorOrNull());
875
+ const { definition: policyDefinition, catalogEntry: versionedCatalogEntry } = definitionResult.getOrNull();
876
+ this.#reportSafely({
877
+ kind: "policy-resolution",
878
+ level: "info",
879
+ policyKey,
880
+ definitionId: policyDefinition.id,
881
+ policyVersion: policyDefinition.policyVersion,
882
+ engineKind: policyDefinition.isGate() ? "GATE" : "COMPUTE",
883
+ engineVersion: policyDefinition.isGate() ? policyDefinition.gateEngineVersion : policyDefinition.computeEngineVersion,
884
+ occurredAt: now
885
+ });
886
+ const ctxResult = await this.contextBuilder.build(versionedCatalogEntry.contextRequirements, seed);
887
+ if (ctxResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidContext("CONTEXT_BUILD", policyKey, ctxResult.errorOrNull()));
888
+ const context = ctxResult.getOrNull();
889
+ const decisionResult = this.#executeEngine(policyKey, policyDefinition, context);
890
+ if (decisionResult.isErr()) return Result.err(decisionResult.errorOrNull());
891
+ const decision = decisionResult.getOrNull();
892
+ const result = {
893
+ decisionId: input.decisionId,
894
+ ref: {
895
+ definitionId: policyDefinition.id,
896
+ policyKey: policyDefinition.policyKey,
897
+ policyVersion: policyDefinition.policyVersion,
898
+ payloadHash: policyDefinition.payloadHash,
899
+ ...policyDefinition.isGate() ? { gateEngineVersion: policyDefinition.gateEngineVersion } : { computeEngineVersion: policyDefinition.computeEngineVersion }
900
+ },
901
+ kind: policyDefinition.kind,
902
+ asOf,
903
+ evaluatedAt: now,
904
+ contextVersion: input.contextVersion,
905
+ decision
906
+ };
907
+ return Result.ok(result);
908
+ }
909
+ #resolveCatalogVariant(definition) {
910
+ return this.catalog.getVersioned({
911
+ key: definition.policyKey,
912
+ kind: definition.kind,
913
+ ...definition.isGate() ? { gateEngineVersion: definition.gateEngineVersion } : { computeEngineVersion: definition.computeEngineVersion },
914
+ payloadSchemaVersion: definition.payloadSchemaVersion
915
+ });
916
+ }
917
+ #resolveCatalogFamily(rawKey) {
918
+ const keyResult = PolicyKey.parse(rawKey);
919
+ if (keyResult.isErr()) return Result.err(PolicyEvaluationErrors.invalidPolicyKey(rawKey, keyResult.errorOrNull()));
920
+ const policyKey = keyResult.getOrNull().toString();
921
+ const catalogFamilyResult = this.catalog.getFamily(policyKey);
922
+ if (catalogFamilyResult.isErr()) return Result.err(PolicyEvaluationErrors.policyNotFound(policyKey, catalogFamilyResult.errorOrNull()));
923
+ return Result.ok({
924
+ key: policyKey,
925
+ family: catalogFamilyResult.getOrNull()
926
+ });
927
+ }
928
+ #findCandidates(policyKey, policyKind, asOf, input) {
929
+ const candidates = this.defRepo.findCandidates({
930
+ policyKey,
931
+ kind: policyKind,
932
+ asOf,
933
+ contextVersion: input.contextVersion,
934
+ scopeChain: input.scopeChain
935
+ });
936
+ const compatibleCandidates = [];
937
+ const compatibleCandidatesByDefinitionId = /* @__PURE__ */ new Map();
938
+ const incompatibleVariantErrorsByDefinitionId = /* @__PURE__ */ new Map();
939
+ for (const candidate of candidates) {
940
+ const compatibilityResult = this.#resolveCatalogVariant(candidate);
941
+ if (compatibilityResult.isErr()) {
942
+ incompatibleVariantErrorsByDefinitionId.set(candidate.id, compatibilityResult.errorOrNull());
943
+ continue;
944
+ }
945
+ const resolvedCandidate = {
946
+ definition: candidate,
947
+ catalogEntry: compatibilityResult.getOrNull()
948
+ };
949
+ compatibleCandidates.push(resolvedCandidate);
950
+ compatibleCandidatesByDefinitionId.set(candidate.id, resolvedCandidate);
951
+ }
952
+ if (compatibleCandidates.length === 0 && candidates.length > 0) {
953
+ const selectedCandidateResult = this.resolver.resolveBest(candidates);
954
+ if (selectedCandidateResult.isOk()) {
955
+ const selectedCandidate = selectedCandidateResult.getOrNull();
956
+ return Result.err(PolicyEvaluationErrors.policyVariantNotFound({
957
+ policyKey,
958
+ policyKind: selectedCandidate.kind,
959
+ payloadSchemaVersion: selectedCandidate.payloadSchemaVersion,
960
+ ...selectedCandidate.isGate() ? { gateEngineVersion: selectedCandidate.gateEngineVersion } : { computeEngineVersion: selectedCandidate.computeEngineVersion },
961
+ cause: incompatibleVariantErrorsByDefinitionId.get(selectedCandidate.id) ?? "No compatible catalog variant matched the selected policy definition"
962
+ }));
963
+ }
964
+ }
965
+ const bestResult = this.resolver.resolveBest(compatibleCandidates.map((candidate) => candidate.definition));
966
+ if (bestResult.isErr()) return Result.err(PolicyEvaluationErrors.policyDefinitionNotFound(policyKey, asOf, input.contextVersion, bestResult.errorOrNull()));
967
+ const bestDefinition = bestResult.getOrNull();
968
+ const resolvedCandidate = compatibleCandidatesByDefinitionId.get(bestDefinition.id);
969
+ if (resolvedCandidate === void 0) return Result.err(PolicyEvaluationErrors.policyDefinitionNotFound(policyKey, asOf, input.contextVersion, `Resolved definition "${bestDefinition.id}" is not present among compatible catalog variants`));
970
+ return Result.ok(resolvedCandidate);
971
+ }
972
+ #executeEngine(policyKey, definition, context) {
973
+ if (definition.isGate()) {
974
+ const gateResult = this.gateEngines.evaluate(definition.gateEngineVersion, definition.payloadJson, context);
975
+ if (gateResult.isErr()) return Result.err(PolicyEvaluationErrors.engineFailure(policyKey, "GATE", definition.gateEngineVersion, gateResult.errorOrNull()));
976
+ return Result.ok(gateResult.getOrNull());
977
+ }
978
+ if (!definition.isCompute()) return Result.err(PolicyEvaluationErrors.policyVariantNotFound({
979
+ policyKey,
980
+ policyKind: definition.kind,
981
+ payloadSchemaVersion: definition.payloadSchemaVersion,
982
+ cause: `Unsupported policy kind "${definition.kind}"`
983
+ }));
984
+ const computeResult = this.computeRegistry.evaluate(policyKey, definition.computeEngineVersion, definition.payloadSchemaVersion, definition.payloadJson, context);
985
+ if (computeResult.isErr()) return Result.err(PolicyEvaluationErrors.engineFailure(policyKey, "COMPUTE", definition.computeEngineVersion, computeResult.errorOrNull()));
986
+ return Result.ok(computeResult.getOrNull());
987
+ }
988
+ };
989
+ //#endregion
990
+ export { asPolicyDecisionId as _, ContextSeedValidator as a, asTenantId as b, ContextResolverRegistry as c, PolicyDefinition as d, InMemoryPolicyDefinitionRepository as f, PolicyCatalog as g, PolicyCatalogFactory as h, PolicyAsOfResolver as i, registerNamespacedContextResolversIn as l, PolicyKey as m, PolicyEvaluationErrors as n, contextResolverRegistry as o, PolicyScopeMatcher as p, PolicyResolver as r, registerNamespacedContextResolvers as s, PolicyService as t, PolicyContextBuilder as u, asPolicyDefinitionId as v, PolicyHashing as x, asSchoolId as y };
991
+
992
+ //# sourceMappingURL=policy-service.js.map