@cullet/erp-core 1.5.0 → 2.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 (306) hide show
  1. package/KIT_CONTEXT.md +2 -2
  2. package/dist/abac/index.d.cts +2 -2
  3. package/dist/abac/index.d.ts +2 -2
  4. package/dist/aggregate-version.cjs +14 -0
  5. package/dist/aggregate-version.cjs.map +1 -0
  6. package/dist/aggregate-version.js +9 -0
  7. package/dist/aggregate-version.js.map +1 -0
  8. package/dist/app-error.cjs +8 -1
  9. package/dist/app-error.cjs.map +1 -1
  10. package/dist/app-error.js +8 -1
  11. package/dist/app-error.js.map +1 -1
  12. package/dist/application/index.cjs +8 -4
  13. package/dist/application/index.d.cts +2 -2
  14. package/dist/application/index.d.ts +2 -2
  15. package/dist/application/index.js +1 -2
  16. package/dist/authorization-error.cjs +67 -17
  17. package/dist/authorization-error.cjs.map +1 -1
  18. package/dist/authorization-error.d.cts +6 -2
  19. package/dist/authorization-error.d.ts +6 -2
  20. package/dist/authorization-error.js +50 -18
  21. package/dist/authorization-error.js.map +1 -1
  22. package/dist/authorizer.port.d.cts +18 -3
  23. package/dist/authorizer.port.d.ts +18 -3
  24. package/dist/composite-authorizer.d.cts +63 -10
  25. package/dist/composite-authorizer.d.ts +63 -10
  26. package/dist/condition-evaluator.cjs +115 -184
  27. package/dist/condition-evaluator.cjs.map +1 -1
  28. package/dist/condition-evaluator.js +116 -179
  29. package/dist/condition-evaluator.js.map +1 -1
  30. package/dist/core-config.d.cts +53 -6
  31. package/dist/core-config.d.ts +53 -6
  32. package/dist/decorate.cjs +14 -0
  33. package/dist/decorate.js +9 -0
  34. package/dist/domain/index.cjs +107 -2
  35. package/dist/domain/index.cjs.map +1 -0
  36. package/dist/domain/index.d.cts +25 -1
  37. package/dist/domain/index.d.ts +25 -1
  38. package/dist/domain/index.js +99 -3
  39. package/dist/domain/index.js.map +1 -0
  40. package/dist/domain-event-contracts.cjs +12 -8
  41. package/dist/domain-event-contracts.cjs.map +1 -1
  42. package/dist/domain-event-contracts.d.cts +29 -4
  43. package/dist/domain-event-contracts.d.ts +29 -4
  44. package/dist/domain-event-contracts.js +11 -7
  45. package/dist/domain-event-contracts.js.map +1 -1
  46. package/dist/domain-exception.cjs +2 -1
  47. package/dist/domain-exception.cjs.map +1 -1
  48. package/dist/domain-exception.js +2 -1
  49. package/dist/domain-exception.js.map +1 -1
  50. package/dist/errors/index.cjs +1 -1
  51. package/dist/errors/index.d.cts +1 -1
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +2 -2
  54. package/dist/exceptions/index.cjs +2 -2
  55. package/dist/exceptions/index.d.cts +1 -2
  56. package/dist/exceptions/index.d.ts +1 -2
  57. package/dist/exceptions/index.js +2 -2
  58. package/dist/gate-engine-registry.cjs.map +1 -1
  59. package/dist/gate-engine-registry.d.cts +1 -1
  60. package/dist/gate-engine-registry.d.ts +1 -1
  61. package/dist/gate-engine-registry.js.map +1 -1
  62. package/dist/gate-v1-payload.schema.cjs +11 -6
  63. package/dist/gate-v1-payload.schema.cjs.map +1 -1
  64. package/dist/gate-v1-payload.schema.js +11 -6
  65. package/dist/gate-v1-payload.schema.js.map +1 -1
  66. package/dist/hashing.cjs +1 -1
  67. package/dist/hashing.cjs.map +1 -1
  68. package/dist/hashing.js +2 -2
  69. package/dist/hashing.js.map +1 -1
  70. package/dist/immutable.cjs +25 -12
  71. package/dist/immutable.cjs.map +1 -1
  72. package/dist/immutable.js +24 -11
  73. package/dist/immutable.js.map +1 -1
  74. package/dist/index.cjs +13 -10
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +9 -10
  77. package/dist/index.d.ts +9 -10
  78. package/dist/index.js +7 -9
  79. package/dist/index.js.map +1 -1
  80. package/dist/invalid-state-transition-exception.cjs +6 -6
  81. package/dist/invalid-state-transition-exception.cjs.map +1 -1
  82. package/dist/invalid-state-transition-exception.js +6 -6
  83. package/dist/invalid-state-transition-exception.js.map +1 -1
  84. package/dist/invariant-violation-exception.cjs +2 -2
  85. package/dist/invariant-violation-exception.cjs.map +1 -1
  86. package/dist/invariant-violation-exception.js +2 -2
  87. package/dist/invariant-violation-exception.js.map +1 -1
  88. package/dist/not-found-error.cjs +6 -4
  89. package/dist/not-found-error.cjs.map +1 -1
  90. package/dist/not-found-error.js +6 -4
  91. package/dist/not-found-error.js.map +1 -1
  92. package/dist/outcome.cjs +5 -5
  93. package/dist/outcome.cjs.map +1 -1
  94. package/dist/outcome.js +5 -5
  95. package/dist/outcome.js.map +1 -1
  96. package/dist/parse-gate-payload.d.cts +1 -1
  97. package/dist/parse-gate-payload.d.ts +1 -1
  98. package/dist/path.d.cts +2 -2
  99. package/dist/path.d.ts +2 -2
  100. package/dist/plugin.cjs +23 -13
  101. package/dist/plugin.cjs.map +1 -1
  102. package/dist/plugin.d.cts +22 -8
  103. package/dist/plugin.d.ts +22 -8
  104. package/dist/plugin.js +23 -13
  105. package/dist/plugin.js.map +1 -1
  106. package/dist/policies/engines/index.d.cts +1 -1
  107. package/dist/policies/engines/index.d.ts +1 -1
  108. package/dist/policies/engines/v1/gate/index.d.cts +3 -15
  109. package/dist/policies/engines/v1/gate/index.d.ts +3 -15
  110. package/dist/policies/index.d.cts +1 -1
  111. package/dist/policies/index.d.ts +1 -1
  112. package/dist/policy-bridge.cjs +30 -2
  113. package/dist/policy-bridge.cjs.map +1 -1
  114. package/dist/policy-bridge.d.cts +18 -0
  115. package/dist/policy-bridge.d.ts +18 -0
  116. package/dist/policy-bridge.js +30 -2
  117. package/dist/policy-bridge.js.map +1 -1
  118. package/dist/policy-service.cjs +40 -46
  119. package/dist/policy-service.cjs.map +1 -1
  120. package/dist/policy-service.d.cts +72 -9
  121. package/dist/policy-service.d.ts +72 -9
  122. package/dist/policy-service.js +42 -48
  123. package/dist/policy-service.js.map +1 -1
  124. package/dist/requested-by.cjs +1 -1
  125. package/dist/requested-by.cjs.map +1 -1
  126. package/dist/requested-by.js +1 -1
  127. package/dist/requested-by.js.map +1 -1
  128. package/dist/result.cjs +29 -1
  129. package/dist/result.cjs.map +1 -1
  130. package/dist/result.d.cts +12 -0
  131. package/dist/result.d.ts +12 -0
  132. package/dist/result.js +29 -1
  133. package/dist/result.js.map +1 -1
  134. package/dist/rule.cjs +94 -24
  135. package/dist/rule.cjs.map +1 -1
  136. package/dist/rule.js +94 -24
  137. package/dist/rule.js.map +1 -1
  138. package/dist/ruleset-registry.cjs +19 -0
  139. package/dist/ruleset-registry.cjs.map +1 -1
  140. package/dist/ruleset-registry.js +19 -0
  141. package/dist/ruleset-registry.js.map +1 -1
  142. package/dist/stable-stringify.cjs +43 -3
  143. package/dist/stable-stringify.cjs.map +1 -1
  144. package/dist/stable-stringify.js +38 -4
  145. package/dist/stable-stringify.js.map +1 -1
  146. package/dist/temporal-snapshot.d.cts +87 -0
  147. package/dist/temporal-snapshot.d.ts +87 -0
  148. package/dist/temporal-use-case.cjs +169 -16
  149. package/dist/temporal-use-case.cjs.map +1 -1
  150. package/dist/temporal-use-case.d.cts +76 -43
  151. package/dist/temporal-use-case.d.ts +76 -43
  152. package/dist/temporal-use-case.js +162 -15
  153. package/dist/temporal-use-case.js.map +1 -1
  154. package/dist/unexpected-error.cjs +4 -2
  155. package/dist/unexpected-error.cjs.map +1 -1
  156. package/dist/unexpected-error.js +4 -2
  157. package/dist/unexpected-error.js.map +1 -1
  158. package/dist/uuid-identifier.cjs +140 -2
  159. package/dist/uuid-identifier.cjs.map +1 -1
  160. package/dist/uuid-identifier.d.cts +26 -7
  161. package/dist/uuid-identifier.d.ts +26 -7
  162. package/dist/uuid-identifier.js +135 -3
  163. package/dist/uuid-identifier.js.map +1 -1
  164. package/dist/uuid.cjs +11 -6
  165. package/dist/uuid.cjs.map +1 -1
  166. package/dist/uuid.js +11 -6
  167. package/dist/uuid.js.map +1 -1
  168. package/dist/validation-code.cjs +9 -0
  169. package/dist/validation-code.cjs.map +1 -1
  170. package/dist/validation-code.d.cts +3 -0
  171. package/dist/validation-code.d.ts +3 -0
  172. package/dist/validation-code.js +9 -0
  173. package/dist/validation-code.js.map +1 -1
  174. package/dist/validation-error.cjs +42 -67
  175. package/dist/validation-error.cjs.map +1 -1
  176. package/dist/validation-error.d.cts +24 -8
  177. package/dist/validation-error.d.ts +24 -8
  178. package/dist/validation-error.js +41 -66
  179. package/dist/validation-error.js.map +1 -1
  180. package/dist/validation-exception.cjs +17 -6
  181. package/dist/validation-exception.cjs.map +1 -1
  182. package/dist/validation-exception.d.cts +33 -9
  183. package/dist/validation-exception.d.ts +33 -9
  184. package/dist/validation-exception.js +17 -6
  185. package/dist/validation-exception.js.map +1 -1
  186. package/dist/validation-field.cjs +3 -0
  187. package/dist/validation-field.cjs.map +1 -1
  188. package/dist/validation-field.d.cts +1 -0
  189. package/dist/validation-field.d.ts +1 -0
  190. package/dist/validation-field.js +3 -0
  191. package/dist/validation-field.js.map +1 -1
  192. package/dist/value-object-ruleset.contracts.d.cts +10 -0
  193. package/dist/value-object-ruleset.contracts.d.ts +10 -0
  194. package/dist/value-object.cjs +12 -2
  195. package/dist/value-object.cjs.map +1 -1
  196. package/dist/value-object.d.cts +16 -0
  197. package/dist/value-object.d.ts +16 -0
  198. package/dist/value-object.js +13 -3
  199. package/dist/value-object.js.map +1 -1
  200. package/dist/version.d.cts +27 -0
  201. package/dist/version.d.ts +27 -0
  202. package/dist/versioning/index.d.cts +2 -2
  203. package/dist/versioning/index.d.ts +2 -2
  204. package/meta.json +3 -2
  205. package/package.json +1 -1
  206. package/src/core/abac/authorizer.ts +60 -10
  207. package/src/core/abac/domain/policy-set.ts +52 -5
  208. package/src/core/abac/domain/rule.ts +28 -16
  209. package/src/core/abac/index.ts +7 -1
  210. package/src/core/application/commands/command.ts +14 -1
  211. package/src/core/application/commands/requested-by.ts +12 -3
  212. package/src/core/application/index.ts +2 -1
  213. package/src/core/application/policy-error-mapper.ts +6 -0
  214. package/src/core/application/ports/index.ts +7 -0
  215. package/src/core/application/ports/temporal-repository.port.ts +35 -2
  216. package/src/core/application/queries/index.ts +1 -1
  217. package/src/core/application/queries/query.ts +19 -2
  218. package/src/core/application/temporal/temporal-use-case.ts +31 -4
  219. package/src/core/application/use-case.ts +44 -7
  220. package/src/core/config/core-config.ts +46 -25
  221. package/src/core/config/index.ts +1 -0
  222. package/src/core/config/policy-reporter.ts +32 -1
  223. package/src/core/domain/entity.ts +50 -7
  224. package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
  225. package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
  226. package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
  227. package/src/core/domain/temporal/half-open-interval.ts +34 -0
  228. package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
  229. package/src/core/domain/temporal/transaction-time.ts +19 -15
  230. package/src/core/domain/temporal/valid-time.ts +20 -15
  231. package/src/core/domain/uuid-identifier.ts +5 -3
  232. package/src/core/domain/value-object.ts +17 -0
  233. package/src/core/errors/authentication-error.ts +5 -31
  234. package/src/core/errors/authorization-error.ts +12 -20
  235. package/src/core/errors/business-rule-violation-error.ts +4 -1
  236. package/src/core/errors/idempotency-error.ts +5 -2
  237. package/src/core/errors/index.ts +4 -0
  238. package/src/core/errors/integration-error.ts +4 -24
  239. package/src/core/errors/legacy-incompatible-error.ts +1 -0
  240. package/src/core/errors/not-found-error.ts +4 -1
  241. package/src/core/errors/temporal-error.ts +22 -20
  242. package/src/core/errors/unexpected-error.ts +5 -1
  243. package/src/core/errors/utils/factory-helpers.ts +70 -0
  244. package/src/core/errors/utils/index.ts +5 -0
  245. package/src/core/errors/utils/json-safe.ts +15 -1
  246. package/src/core/errors/validation-error.ts +4 -1
  247. package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
  248. package/src/core/exceptions/domain-exception.ts +9 -1
  249. package/src/core/exceptions/entity-not-found-exception.ts +5 -1
  250. package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
  251. package/src/core/exceptions/invariant-violation-exception.ts +2 -2
  252. package/src/core/exceptions/validation-code.ts +14 -0
  253. package/src/core/exceptions/validation-exception.ts +44 -5
  254. package/src/core/exceptions/validation-field.ts +11 -1
  255. package/src/core/plugins/plugin.ts +25 -15
  256. package/src/core/plugins/types.ts +4 -2
  257. package/src/core/policies/asof/asof.ts +12 -0
  258. package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
  259. package/src/core/policies/catalog/policy-catalog.ts +5 -1
  260. package/src/core/policies/context/context-builder.ts +20 -0
  261. package/src/core/policies/context/context-resolver.ts +5 -0
  262. package/src/core/policies/context/context-seed.ts +11 -0
  263. package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
  264. package/src/core/policies/engines/parse-gate-payload.ts +9 -0
  265. package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
  266. package/src/core/policies/engines/v1/condition-evaluator.ts +99 -325
  267. package/src/core/policies/engines/v1/condition-schema.ts +23 -19
  268. package/src/core/policies/engines/v1/condition-types.ts +18 -11
  269. package/src/core/policies/index.ts +4 -6
  270. package/src/core/policies/service/policy-service.ts +46 -35
  271. package/src/core/policies/utils/hash.ts +5 -69
  272. package/src/core/policies/utils/result.ts +1 -1
  273. package/src/core/rbac/access-request.ts +12 -2
  274. package/src/core/rbac/authorizer.ts +8 -1
  275. package/src/core/rbac/domain/role.ts +20 -0
  276. package/src/core/rbac/domain/scope.ts +6 -1
  277. package/src/core/result/index.ts +5 -0
  278. package/src/core/result/outcome.ts +5 -7
  279. package/src/core/result/result.ts +34 -1
  280. package/src/core/shared/hashing.ts +6 -3
  281. package/src/core/shared/immutable.ts +22 -4
  282. package/src/core/shared/stable-stringify.ts +91 -4
  283. package/src/core/shared/uuid.ts +11 -6
  284. package/src/core/versioning/domain-event-contracts.ts +43 -15
  285. package/src/core/versioning/index.ts +1 -0
  286. package/src/core/versioning/version.ts +30 -1
  287. package/src/domain/index.ts +5 -0
  288. package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
  289. package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
  290. package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
  291. package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
  292. package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
  293. package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
  294. package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
  295. package/src/result/index.ts +7 -2
  296. package/src/version.ts +1 -1
  297. package/dist/domain-exception.d.cts +0 -7
  298. package/dist/domain-exception.d.ts +0 -7
  299. package/dist/entity.cjs +0 -122
  300. package/dist/entity.cjs.map +0 -1
  301. package/dist/entity.js +0 -111
  302. package/dist/entity.js.map +0 -1
  303. package/dist/use-case.cjs +0 -92
  304. package/dist/use-case.cjs.map +0 -1
  305. package/dist/use-case.js +0 -87
  306. package/dist/use-case.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  import { type ContractVersion, version } from "../versioning/version.js";
2
+ import { AppError } from "../errors/index.js";
2
3
  import type { Result } from "../result/result.js";
3
4
 
4
5
  import type { LoggerPort } from "./ports/logger.port.js";
5
6
  import type { MetricsPort } from "./ports/metrics.port.js";
6
- import type { TracerPort } from "./ports/tracer.port.js";
7
+ import type { TraceAttributeValue, TracerPort } from "./ports/tracer.port.js";
7
8
 
8
9
  type MaybePromise<T> = T | Promise<T>;
9
10
 
@@ -82,25 +83,45 @@ abstract class UseCase<
82
83
 
83
84
  protected abstract execute(input: Input): MaybePromise<Output>;
84
85
 
86
+ /**
87
+ * Extra attributes stamped onto the span and every metric emitted for this
88
+ * execution. Override to attach stable, **low-cardinality** dimensions
89
+ * (e.g. `Command` adds `requested_by.kind`). Keep values bounded — they
90
+ * become metric label values, so a user id or free-form string here would
91
+ * explode the metric's cardinality. Defaults to none.
92
+ */
93
+ protected spanAttributes(
94
+ _input: Input,
95
+ ): Readonly<Record<string, TraceAttributeValue>> {
96
+ return {};
97
+ }
98
+
85
99
  private async runInstrumented(
86
100
  input: Input,
87
101
  observability: UseCaseObservability,
88
102
  ): Promise<Output> {
89
103
  const { logger, metrics, tracer } = observability;
90
104
  const name = this.useCaseName;
105
+ const attributes = safely(() => this.spanAttributes(input)) ?? {};
91
106
  const span = safely(() =>
92
- tracer?.startSpan(name, { "use_case.name": name }),
107
+ tracer?.startSpan(name, { "use_case.name": name, ...attributes }),
93
108
  );
94
- const startedAt = Date.now();
109
+ const startedAt = performance.now();
110
+ // Declared here (not inside `try`) so the `finally` histogram can label
111
+ // duration by outcome; defaults to "exception" and is overwritten once
112
+ // execute() returns without throwing.
113
+ let outcome: ExecutionOutcome = "exception";
95
114
 
96
115
  try {
97
116
  const output = await this.execute(input);
98
- const outcome: ExecutionOutcome = output.isOk() ? "ok" : "error";
117
+ outcome = output.isOk() ? "ok" : "error";
99
118
 
100
119
  if (outcome === "error") {
120
+ const code = businessErrorCode(output);
101
121
  safely(() =>
102
122
  logger?.warn(`${name} returned a business error`, {
103
123
  useCase: name,
124
+ ...(code ? { code } : {}),
104
125
  }),
105
126
  );
106
127
  }
@@ -109,6 +130,7 @@ abstract class UseCase<
109
130
  metrics?.counter(EXECUTION_COUNTER, 1, {
110
131
  useCase: name,
111
132
  outcome,
133
+ ...attributes,
112
134
  }),
113
135
  );
114
136
 
@@ -125,21 +147,36 @@ abstract class UseCase<
125
147
  metrics?.counter(EXECUTION_COUNTER, 1, {
126
148
  useCase: name,
127
149
  outcome: "exception",
150
+ ...attributes,
128
151
  }),
129
152
  );
130
153
 
131
154
  throw error;
132
155
  } finally {
133
156
  safely(() =>
134
- metrics?.histogram(DURATION_HISTOGRAM, Date.now() - startedAt, {
135
- useCase: name,
136
- }),
157
+ metrics?.histogram(
158
+ DURATION_HISTOGRAM,
159
+ performance.now() - startedAt,
160
+ { useCase: name, outcome, ...attributes },
161
+ ),
137
162
  );
138
163
  safely(() => span?.end());
139
164
  }
140
165
  }
141
166
  }
142
167
 
168
+ /**
169
+ * Pulls the stable `code` off a business-error `Result` for logging. Returns
170
+ * `undefined` when the error is not an {@link AppError} — the code is safe to
171
+ * log (it's the public contract), unlike the message which may carry PII.
172
+ */
173
+ function businessErrorCode(
174
+ output: Result<unknown, unknown>,
175
+ ): string | undefined {
176
+ const error = output.errorOrNull();
177
+ return error instanceof AppError ? error.code : undefined;
178
+ }
179
+
143
180
  /**
144
181
  * Invokes an observability side effect, isolating any adapter failure so it
145
182
  * cannot alter the use case outcome.
@@ -1,5 +1,7 @@
1
1
  import type {
2
2
  ConditionEvaluationOptions,
3
+ ConditionEvaluationReport,
4
+ ConditionEvaluationReportLevel,
3
5
  ConditionEvaluatorReporter,
4
6
  } from "../policies/engines/condition-evaluator-reporter.js";
5
7
  import type { PolicyEvent, PolicyReporter } from "./policy-reporter.js";
@@ -10,23 +12,32 @@ export interface CoreObservabilityConfig {
10
12
  }
11
13
 
12
14
  export interface CoreConfigOptions {
15
+ // `observability` is today's only section. Future core config (settings,
16
+ // feature flags) joins here as sibling keys — that umbrella is why the
17
+ // reporter is nested under `observability` instead of living at the root.
13
18
  readonly observability?: CoreObservabilityConfig;
14
19
  }
15
20
 
16
- function reportSafely(
17
- policyReporter: PolicyReporter,
18
- event: PolicyEvent,
19
- ): void {
21
+ /** Reports an event without letting a throwing reporter abort the caller. */
22
+ function safeReport(policyReporter: PolicyReporter, event: PolicyEvent): void {
20
23
  try {
21
24
  policyReporter.report(event);
22
25
  } catch {
23
- // Falhas de telemetria nao podem interromper o fluxo de dominio.
26
+ // Telemetry failures must never interrupt the domain flow.
24
27
  }
25
28
  }
26
29
 
27
30
  /**
28
- * Centraliza a configuracao pura do core sem depender de implementacoes
29
- * concretas de logging, tracing ou report.
31
+ * The core's config namespace. Today it holds a single section —
32
+ * `observability` whose whole job is to decide *where policy telemetry goes*
33
+ * while guaranteeing telemetry is best-effort (a throwing reporter can never
34
+ * abort a domain flow). App config (env, settings, feature flags) is
35
+ * deliberately absent for now: when the core needs it, it joins here as a
36
+ * sibling section rather than a new module — which is why this is `config`,
37
+ * not `observability`. It holds the active {@link PolicyReporter} (silent by
38
+ * default, so the core carries no logging dependency), bridges it to the
39
+ * engines' {@link ConditionEvaluatorReporter}, and offers `reportSafely` as
40
+ * the single guarded emit path shared with {@link PolicyService}.
30
41
  */
31
42
  export class CoreConfig {
32
43
  readonly #initialReporter: PolicyReporter;
@@ -39,22 +50,30 @@ export class CoreConfig {
39
50
  this.#currentReporter = reporter;
40
51
  }
41
52
 
53
+ /**
54
+ * Swaps the active reporter. Pass `observability.reporter` to enable
55
+ * telemetry, or set it explicitly to `undefined` to go silent again.
56
+ * Omitting `observability` (or its `reporter` key) is a no-op.
57
+ */
42
58
  configure(options: CoreConfigOptions): this {
43
- const reporter = options.observability?.reporter;
44
- if (reporter) {
45
- this.#currentReporter = reporter;
59
+ const observability = options.observability;
60
+ if (observability && "reporter" in observability) {
61
+ this.#currentReporter =
62
+ observability.reporter ?? new SilentPolicyReporter();
46
63
  }
47
64
  return this;
48
65
  }
49
66
 
50
- // Restores the reporter captured at construction time.
67
+ // Restores the reporter captured at construction time — which is silent
68
+ // only if this instance was built without one.
51
69
  reset(): this {
52
70
  this.#currentReporter = this.#initialReporter;
53
71
  return this;
54
72
  }
55
73
 
56
- getPolicyReporter(): PolicyReporter {
57
- return this.#currentReporter;
74
+ /** Emits a policy event through the active reporter, best-effort. */
75
+ reportSafely(event: PolicyEvent): void {
76
+ safeReport(this.#currentReporter, event);
58
77
  }
59
78
 
60
79
  getConditionEvaluationOptions(
@@ -69,19 +88,21 @@ export class CoreConfig {
69
88
  #bridgeToConditionReporter(
70
89
  policyReporter: PolicyReporter,
71
90
  ): ConditionEvaluatorReporter {
91
+ // Capture the reporter now so a mid-evaluation reconfigure never splits
92
+ // a single evaluation's reports across two reporters (intentional).
93
+ const emit = (
94
+ level: ConditionEvaluationReportLevel,
95
+ report: ConditionEvaluationReport,
96
+ ): void => {
97
+ safeReport(policyReporter, {
98
+ kind: "condition-eval",
99
+ ...report,
100
+ level,
101
+ });
102
+ };
72
103
  return {
73
- warn(report) {
74
- reportSafely(policyReporter, {
75
- kind: "condition-eval",
76
- ...report,
77
- } satisfies PolicyEvent);
78
- },
79
- error(report) {
80
- reportSafely(policyReporter, {
81
- kind: "condition-eval",
82
- ...report,
83
- } satisfies PolicyEvent);
84
- },
104
+ warn: (report) => emit("warn", report),
105
+ error: (report) => emit("error", report),
85
106
  };
86
107
  }
87
108
  }
@@ -11,5 +11,6 @@ export type {
11
11
  PolicyResolutionEvent,
12
12
  PolicyEvaluationFailedEvent,
13
13
  PolicyEvaluationCompletedEvent,
14
+ AbacDecisionEvent,
14
15
  } from "./policy-reporter.js";
15
16
  export { SilentPolicyReporter } from "./silent-policy-reporter.js";
@@ -40,14 +40,45 @@ export interface PolicyEvaluationCompletedEvent {
40
40
  readonly occurredAt: Date;
41
41
  }
42
42
 
43
+ /**
44
+ * A resolved ABAC decision (both PERMIT and DENY). Emitted best-effort by the
45
+ * `AbacAuthorizer` so a host can build an authorization audit trail without an
46
+ * adapter of its own; silent by default like every other event. Types are kept
47
+ * self-contained (no import from `core/abac`) to avoid a config→abac cycle.
48
+ */
49
+ export interface AbacDecisionEvent {
50
+ readonly kind: "abac-decision";
51
+ readonly level: "info";
52
+ readonly action: string;
53
+ readonly resource?: { readonly type: string; readonly id?: string };
54
+ readonly actorId: string;
55
+ readonly effect: "PERMIT" | "DENY";
56
+ /** Deciding rule id, or `"<default>"` / `"<default-deny>"` when the set's default decided. */
57
+ readonly decidingRuleId: string;
58
+ readonly decidingRuleVersion?: number;
59
+ readonly algorithm: string;
60
+ readonly occurredAt: Date;
61
+ }
62
+
43
63
  export type PolicyEvent =
44
64
  | ConditionEvalEvent
45
65
  | PolicyResolutionEvent
46
66
  | PolicyEvaluationFailedEvent
47
- | PolicyEvaluationCompletedEvent;
67
+ | PolicyEvaluationCompletedEvent
68
+ | AbacDecisionEvent;
48
69
 
49
70
  // ─── Reporter interface ───────────────────────────────────────────────────────
50
71
 
72
+ /**
73
+ * Sink for policy telemetry. The core never logs on its own — it hands typed
74
+ * events here and a host implementation decides what to do with them.
75
+ *
76
+ * Redaction is the host's responsibility: a `condition-eval` event carries the
77
+ * evaluated field path and value in `details` (e.g. `student.flags.*`), so in
78
+ * an ERP those payloads can contain PII. Sanitize before persisting or shipping
79
+ * events off-box; the core deliberately keeps no redaction seam so it stays
80
+ * pure and unopinionated about your logging stack.
81
+ */
51
82
  export interface PolicyReporter {
52
83
  report(event: PolicyEvent): void;
53
84
  }
@@ -3,6 +3,8 @@ import { assertValidAggregateVersion } from "../shared/aggregate-version.js";
3
3
  import { assertValidDate, cloneDate } from "../shared/temporal-guards.js";
4
4
  import { type ContractVersion, version } from "../versioning/version.js";
5
5
 
6
+ import { ValueObject } from "./value-object.js";
7
+
6
8
  /**
7
9
  * The minimal persisted shape needed to reconstitute an {@link Entity}.
8
10
  *
@@ -53,9 +55,16 @@ abstract class Entity<TIdentifier> {
53
55
  * Declared `protected` because entities are reconstituted through a
54
56
  * subclass factory, never instantiated directly by application code.
55
57
  *
56
- * @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.
58
+ * @throws {InvariantViolationException} When `id` is absent, or when
59
+ * `updatedAt` is earlier than `createdAt`.
57
60
  */
58
61
  protected constructor(state: EntityState<TIdentifier>) {
62
+ if (state.id == null) {
63
+ throw new InvariantViolationException(
64
+ "id is required: an entity cannot exist without an identity",
65
+ );
66
+ }
67
+
59
68
  assertValidDate("createdAt", state.createdAt);
60
69
  assertValidDate("updatedAt", state.updatedAt);
61
70
  assertValidAggregateVersion(state.aggregateVersion);
@@ -121,18 +130,23 @@ abstract class Entity<TIdentifier> {
121
130
  * call this from every state-changing method so the version counter stays
122
131
  * an accurate optimistic-lock token.
123
132
  *
124
- * @param updatedAt - The modification instant; defaults to now. Validated
125
- * and required not to predate `createdAt`, preserving the same invariant
126
- * the constructor enforces.
133
+ * `updatedAt` is monotonic: each call must be at or after the current
134
+ * `updatedAt` (equal is fine two mutations can share a millisecond).
135
+ * Without this, the version counter would advance while the timestamp
136
+ * walked backwards, corrupting any audit trail or ordering that reads
137
+ * `updatedAt`. A caller replaying out-of-order events must sort them first.
138
+ *
139
+ * @param updatedAt - The modification instant; defaults to now.
127
140
  * @returns The new aggregate version after the increment.
128
- * @throws {InvariantViolationException} When `updatedAt` is earlier than `createdAt`.
141
+ * @throws {InvariantViolationException} When `updatedAt` is earlier than
142
+ * the current `updatedAt`.
129
143
  */
130
144
  protected markAsModified(updatedAt: Date = new Date()): number {
131
145
  assertValidDate("updatedAt", updatedAt);
132
146
 
133
- if (updatedAt.getTime() < this._createdAt.getTime()) {
147
+ if (updatedAt.getTime() < this._updatedAt.getTime()) {
134
148
  throw new InvariantViolationException(
135
- "updatedAt cannot be earlier than createdAt",
149
+ "updatedAt cannot move backwards: it must be at or after the current updatedAt",
136
150
  );
137
151
  }
138
152
 
@@ -141,6 +155,35 @@ abstract class Entity<TIdentifier> {
141
155
 
142
156
  return this._aggregateVersion;
143
157
  }
158
+
159
+ /**
160
+ * Identity-based equality — the semantics that define an entity. Two
161
+ * entities are equal when they are the same concrete class and their ids
162
+ * match, regardless of any other field.
163
+ *
164
+ * The class check keeps a `CustomerId`-bearing entity from equalling an
165
+ * unrelated entity that happens to reuse the same raw id. Value-object ids
166
+ * are compared through their own `equals`, so two independently
167
+ * reconstituted id instances still match; primitive ids use `===`.
168
+ */
169
+ public equals(other: Entity<TIdentifier> | null | undefined): boolean {
170
+ if (other == null) {
171
+ return false;
172
+ }
173
+ if (other === this) {
174
+ return true;
175
+ }
176
+ if (other.constructor !== this.constructor) {
177
+ return false;
178
+ }
179
+
180
+ const id: unknown = this._id;
181
+ if (id instanceof ValueObject) {
182
+ return id.equals(other._id as typeof id);
183
+ }
184
+
185
+ return this._id === other._id;
186
+ }
144
187
  }
145
188
 
146
189
  export { Entity, type EntityState };
@@ -1,5 +1,4 @@
1
1
  import { type Ruleset, type RulesetId } from "./ruleset.contracts.js";
2
- import { DomainException } from "../../exceptions/domain-exception.js";
3
2
 
4
3
  interface CreationRuleset<TData> extends Ruleset {
5
4
  validate(data: TData): void;
@@ -9,7 +8,6 @@ type InvariantRuleset = Ruleset;
9
8
 
10
9
  export {
11
10
  type CreationRuleset,
12
- DomainException,
13
11
  type InvariantRuleset,
14
12
  type Ruleset,
15
13
  type RulesetId,
@@ -6,6 +6,15 @@ import {
6
6
 
7
7
  class RulesetRegistryError extends DomainException {}
8
8
 
9
+ /**
10
+ * Runtime guard for the `RulesetId` shape (`name@major.minor`). The template
11
+ * type only protects TypeScript callers — a JS consumer (or a cast) could
12
+ * register `"foo"` or `"foo@1.5.2"`, and either would silently corrupt
13
+ * {@link RulesetRegistry.getCurrent}'s version ordering via `NaN`/ignored
14
+ * segments. Same idea as `CONTRACT_VERSION_PATTERN` in `versioning/version.ts`.
15
+ */
16
+ const RULESET_ID_PATTERN = /^.+@\d+\.\d+$/;
17
+
9
18
  function parseVersion(id: string): [number, number] {
10
19
  const atIdx = id.lastIndexOf("@");
11
20
  const versionStr = id.slice(atIdx + 1);
@@ -23,6 +32,11 @@ class RulesetRegistry implements RulesetRegistryContract {
23
32
  "Registry is sealed. No new rulesets can be registered.",
24
33
  );
25
34
  }
35
+ if (!RULESET_ID_PATTERN.test(ruleset.id)) {
36
+ throw new RulesetRegistryError(
37
+ `Invalid ruleset id "${ruleset.id}". Expected "<name>@<major>.<minor>", e.g. "order-creation@1.0".`,
38
+ );
39
+ }
26
40
  if (this._store.has(ruleset.id)) {
27
41
  throw new RulesetRegistryError(
28
42
  `Ruleset with id "${ruleset.id}" is already registered.`,
@@ -35,6 +49,12 @@ class RulesetRegistry implements RulesetRegistryContract {
35
49
  this._sealed = true;
36
50
  }
37
51
 
52
+ /**
53
+ * Looks up a ruleset by exact id. The `T` parameter is a caller-asserted
54
+ * cast, not a runtime check: asking for the wrong `T` compiles and returns
55
+ * the object mistyped — the standard registry trade-off. Keep the id and
56
+ * the expected type paired at the call site.
57
+ */
38
58
  get<T extends Ruleset>(id: string): T {
39
59
  const ruleset = this._store.get(id);
40
60
  if (!ruleset) {
@@ -46,6 +66,10 @@ class RulesetRegistry implements RulesetRegistryContract {
46
66
  return ruleset as T;
47
67
  }
48
68
 
69
+ /**
70
+ * Returns the highest `major.minor` version registered under `prefix`.
71
+ * The same caller-asserted `T` cast as {@link get} applies.
72
+ */
49
73
  getCurrent<T extends Ruleset>(prefix: string): T {
50
74
  const matchingEntries = Array.from(this._store.entries()).filter(
51
75
  ([key]) => key.startsWith(prefix + "@"),
@@ -1,13 +1,7 @@
1
- import { DomainException } from "../../exceptions/domain-exception.js";
2
1
  import { type Ruleset, type RulesetId } from "./ruleset.contracts.js";
3
2
 
4
3
  interface ValueObjectRuleset<T> extends Ruleset {
5
4
  validate(value: T): void;
6
5
  }
7
6
 
8
- export {
9
- DomainException,
10
- type Ruleset,
11
- type RulesetId,
12
- type ValueObjectRuleset,
13
- };
7
+ export { type Ruleset, type RulesetId, type ValueObjectRuleset };
@@ -0,0 +1,34 @@
1
+ import { InvariantViolationException } from "../../exceptions/invariant-violation-exception.js";
2
+ import { assertValidDate } from "../../shared/temporal-guards.js";
3
+
4
+ /**
5
+ * Shared core of the bitemporal primitives: a half-open interval
6
+ * `[start, end)` whose end, when present, must be strictly later than its
7
+ * start. `ValidTime` and `TransactionTime` are the same shape under different
8
+ * (deliberate) vocabularies — the invariant lives here once so the two files
9
+ * cannot drift apart.
10
+ *
11
+ * Internal to `domain/temporal`; not exported from the barrel.
12
+ */
13
+ function assertHalfOpenInterval(
14
+ startLabel: string,
15
+ start: Date,
16
+ endLabel: string,
17
+ end: Date | undefined,
18
+ ): void {
19
+ assertValidDate(startLabel, start);
20
+
21
+ if (end === undefined) {
22
+ return;
23
+ }
24
+
25
+ assertValidDate(endLabel, end);
26
+
27
+ if (end.getTime() <= start.getTime()) {
28
+ throw new InvariantViolationException(
29
+ `${endLabel} must be later than ${startLabel}`,
30
+ );
31
+ }
32
+ }
33
+
34
+ export { assertHalfOpenInterval };
@@ -14,6 +14,14 @@ import {
14
14
  /**
15
15
  * Pairs a domain datum with its time dimensions (Business and Transaction).
16
16
  * It is the representation of an entry in the append-only (historical) table.
17
+ *
18
+ * `data` must be plain, structured-cloneable state — the primitive shape of an
19
+ * aggregate, not the aggregate itself. `createTemporalSnapshot` deep-clones it
20
+ * via `structuredClone`: functions and symbols inside `data` throw an
21
+ * `InvariantViolationException`, and class instances (a `ValueObject`, an
22
+ * `Entity`) are NOT rejected — they are silently flattened to plain objects,
23
+ * losing their prototype (and with it `equals`/`toPrimitive`). Project rich
24
+ * objects down first, e.g. with `toPrimitive()`.
17
25
  */
18
26
  interface TemporalSnapshot<T> {
19
27
  /** The data or state captured by the snapshot. */
@@ -33,8 +41,11 @@ interface CreateTemporalSnapshotInput<T> {
33
41
  function createTemporalSnapshot<T>(
34
42
  input: CreateTemporalSnapshotInput<T>,
35
43
  ): TemporalSnapshot<T> {
36
- return makeImmutable({
37
- data: input.data,
44
+ // The time dimensions come out of their factories already cloned and
45
+ // frozen; only `data` still needs the clone+freeze. Freezing the shell by
46
+ // hand avoids a second structuredClone pass over the whole snapshot.
47
+ return Object.freeze({
48
+ data: makeImmutable(input.data),
38
49
  validTime: createValidTime(input.validTime),
39
50
  txTime: createTransactionTime(input.txTime),
40
51
  });
@@ -1,11 +1,22 @@
1
- import { InvariantViolationException } from "../../exceptions/invariant-violation-exception.js";
2
1
  import { makeImmutable } from "../../shared/immutable.js";
3
- import { assertValidDate } from "../../shared/temporal-guards.js";
2
+
3
+ import { assertHalfOpenInterval } from "./half-open-interval.js";
4
4
 
5
5
  /**
6
6
  * Represents transaction time (Transaction Time).
7
7
  * Defines when the system learned about or recorded a piece of information.
8
8
  * Useful for auditing and reconstructing system state at a specific past moment.
9
+ *
10
+ * The window is half-open — `recordedAt` inclusive, `supersededAt` exclusive —
11
+ * and strictly non-empty: `supersededAt` must be later than `recordedAt`, so a
12
+ * record superseded in the same instant it was written is not representable.
13
+ *
14
+ * One caveat mirrors {@link ValueObject}: `createTransactionTime` clones the
15
+ * input dates (no aliasing) but the inner `Date`s are NOT frozen —
16
+ * `Object.freeze` cannot block `setTime`/`setFullYear`, so a caller can still
17
+ * mutate `recordedAt` in place and corrupt the audit record after creation.
18
+ * `readonly` only prevents reassignment. Treat the dates as read-only, or keep
19
+ * instants as ISO/epoch in your own state when that guarantee matters.
9
20
  */
10
21
  interface TransactionTime {
11
22
  /** When the information was first recorded in the system. */
@@ -20,19 +31,12 @@ function assertTransactionTime(
20
31
  txTime: TransactionTime,
21
32
  fieldName: string = "txTime",
22
33
  ): void {
23
- assertValidDate(`${fieldName}.recordedAt`, txTime.recordedAt);
24
-
25
- if (txTime.supersededAt === undefined) {
26
- return;
27
- }
28
-
29
- assertValidDate(`${fieldName}.supersededAt`, txTime.supersededAt);
30
-
31
- if (txTime.supersededAt.getTime() <= txTime.recordedAt.getTime()) {
32
- throw new InvariantViolationException(
33
- `${fieldName}.supersededAt must be later than ${fieldName}.recordedAt`,
34
- );
35
- }
34
+ assertHalfOpenInterval(
35
+ `${fieldName}.recordedAt`,
36
+ txTime.recordedAt,
37
+ `${fieldName}.supersededAt`,
38
+ txTime.supersededAt,
39
+ );
36
40
  }
37
41
 
38
42
  function createTransactionTime(
@@ -1,10 +1,22 @@
1
- import { InvariantViolationException } from "../../exceptions/invariant-violation-exception.js";
2
1
  import { makeImmutable } from "../../shared/immutable.js";
3
- import { assertValidDate } from "../../shared/temporal-guards.js";
2
+
3
+ import { assertHalfOpenInterval } from "./half-open-interval.js";
4
4
 
5
5
  /**
6
6
  * Represents business time (Valid Time).
7
7
  * Defines when a piece of information is considered true or in effect in the real world.
8
+ *
9
+ * The window is half-open — `from` inclusive, `to` exclusive — and strictly
10
+ * non-empty: `to` must be later than `from`, so a zero-duration range `[t, t)`
11
+ * is deliberately not representable. Model an instantaneous event as an open
12
+ * range closed by the next fact, not as an empty window.
13
+ *
14
+ * One caveat mirrors {@link ValueObject}: `createValidTime` clones the input
15
+ * dates (no aliasing) but the inner `Date`s are NOT frozen — `Object.freeze`
16
+ * cannot block `setTime`/`setFullYear`, so a caller can still mutate `from`
17
+ * in place and corrupt the range after creation. `readonly` only prevents
18
+ * reassignment. Treat the dates as read-only, or keep instants as ISO/epoch in
19
+ * your own state when that guarantee matters.
8
20
  */
9
21
  interface ValidTime {
10
22
  /** Start of the validity window (inclusive). */
@@ -19,19 +31,12 @@ function assertValidTime(
19
31
  validTime: ValidTime,
20
32
  fieldName: string = "validTime",
21
33
  ): void {
22
- assertValidDate(`${fieldName}.from`, validTime.from);
23
-
24
- if (validTime.to === undefined) {
25
- return;
26
- }
27
-
28
- assertValidDate(`${fieldName}.to`, validTime.to);
29
-
30
- if (validTime.to.getTime() <= validTime.from.getTime()) {
31
- throw new InvariantViolationException(
32
- `${fieldName}.to must be later than ${fieldName}.from`,
33
- );
34
- }
34
+ assertHalfOpenInterval(
35
+ `${fieldName}.from`,
36
+ validTime.from,
37
+ `${fieldName}.to`,
38
+ validTime.to,
39
+ );
35
40
  }
36
41
 
37
42
  function createValidTime(input: CreateValidTimeInput): ValidTime {
@@ -38,7 +38,7 @@ abstract class UuidIdentifier<
38
38
  * two identifiers with different brands are not interchangeable at the type
39
39
  * level despite sharing the same `string` value.
40
40
  */
41
- protected declare readonly __brand: TBrand;
41
+ declare protected readonly __brand: TBrand;
42
42
 
43
43
  protected constructor(value: string) {
44
44
  super(value);
@@ -54,8 +54,10 @@ abstract class UuidIdentifier<
54
54
  }
55
55
 
56
56
  /**
57
- * Whether `candidate` is a canonical UUID (versions 1–5). The single source
58
- * of truth for the format; concrete identifiers call this from `create`.
57
+ * Whether `candidate` is a canonical UUID (versions 1–8, including the
58
+ * time-ordered v7; nil and max UUIDs are rejected as sentinels). The single
59
+ * source of truth for the format — see `shared/uuid.ts`; concrete
60
+ * identifiers call this from `create`.
59
61
  */
60
62
  public static isValid(candidate: string): boolean {
61
63
  return UUID_PATTERN.test(candidate);