@vytches/ddd-aggregates 0.30.0 → 0.31.0-alpha.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.
package/LLMGUIDE.md CHANGED
@@ -46,23 +46,39 @@ order.commit(); // clear pending events after saving
46
46
 
47
47
  ## Key API
48
48
 
49
- | Export | Kind | Description |
50
- | -------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | `AggregateRoot<TId>` | class | Base aggregate root; manages version, events, capabilities — the _transactional_ root |
52
- | `Entity<TId>` | abstract class | Base for **non-root** domain entities (VF-CANON-001) — identity-based equality, no version/event machinery. Use for `OrderLine`, inner aggregate entities |
53
- | `AggregateBuilder<TId>` | class | Fluent builder for constructing aggregates with capabilities |
54
- | `aggregateBuilder(params)` | function | Shorthand factory for `AggregateBuilder.create(params)` |
55
- | `AggregateError` | class | Domain error for aggregate-level failures |
56
- | `SnapshotCapability` | class | Enables snapshot creation and restoration |
57
- | `AuditCapability` | class | Attaches an audit log to every event applied |
58
- | `VersioningCapability` | class | Registers event upcasters for schema evolution |
59
- | `EventSourcingCapability` | class | Integrates with `IEventStore` for persistence |
60
- | `asSnapshotAggregate(agg)` | function | Casts aggregate, throws if no `SnapshotCapability` |
61
- | `asAuditAggregate(agg)` | function | Casts aggregate, throws if no `AuditCapability` |
62
- | `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
63
- | `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
64
- | `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates |
65
- | `IAggregateRoot<TId>` | interface | Full contract for aggregates |
49
+ | Export | Kind | Description |
50
+ | ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `AggregateRoot<TId>` | class | Base aggregate root; manages version, events, capabilities — the _transactional_ root |
52
+ | `Entity<TId>` | abstract class | Base for **non-root** domain entities (VF-CANON-001) — identity-based equality, no version/event machinery. Use for `OrderLine`, inner aggregate entities |
53
+ | `AggregateBuilder<TId>` | class | Fluent builder for constructing aggregates with capabilities |
54
+ | `aggregateBuilder(params)` | function | Shorthand factory for `AggregateBuilder.create(params)` |
55
+ | `AggregateError` | class | Domain error for aggregate-level failures |
56
+ | `SnapshotCapability` | class | Enables snapshot creation and restoration |
57
+ | `AuditCapability` | class | Attaches an audit log to every event applied |
58
+ | `VersioningCapability` | class | Registers event upcasters for schema evolution |
59
+ | `EventSourcingCapability` | class | Integrates with `IEventStore` for persistence |
60
+ | `asSnapshotAggregate(agg)` | function | Casts aggregate, throws if no `SnapshotCapability` |
61
+ | `asAuditAggregate(agg)` | function | Casts aggregate, throws if no `AuditCapability` |
62
+ | `asVersioningAggregate(agg)` | function | Casts aggregate, throws if no `VersioningCapability` |
63
+ | `asEventSourcingAggregate(agg)` | function | Casts aggregate, throws if no `EventSourcingCapability` |
64
+ | `tryAsSnapshotAggregate(agg)` | function | Non-throwing sibling of `asSnapshotAggregate`; returns `Result.ok(aggregate)` or `Result.fail(AggregateError)` instead of throwing |
65
+ | `tryAsAuditAggregate(agg)` | function | Non-throwing sibling of `asAuditAggregate`; returns a `Result<AggregateWithAuditCapability<TId>, AggregateError>` |
66
+ | `tryAsVersioningAggregate(agg)` | function | Non-throwing sibling of `asVersioningAggregate`; returns a `Result<AggregateWithVersioningCapability<TId>, AggregateError>` |
67
+ | `tryAsEventSourcingAggregate(agg)` | function | Non-throwing sibling of `asEventSourcingAggregate`; returns a `Result<AggregateWithEventSourcingCapability<TId>, AggregateError>` |
68
+ | `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
69
+ | `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
70
+ | `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates; return type of `asSnapshotAggregate`/`tryAsSnapshotAggregate` |
71
+ | `AggregateWithAuditCapability<TId>` | type | Type-safe alias for audit-capable aggregates; return type of `asAuditAggregate`/`tryAsAuditAggregate` |
72
+ | `AggregateWithVersioningCapability<TId>` | type | Type-safe alias for versioning-capable aggregates; return type of `asVersioningAggregate`/`tryAsVersioningAggregate` |
73
+ | `AggregateWithEventSourcingCapability<TId>` | type | Type-safe alias for event-sourcing-capable aggregates; return type of `asEventSourcingAggregate`/`tryAsEventSourcingAggregate` |
74
+ | `IAggregateRoot<TId>` | interface | Full contract for aggregates |
75
+ | `IAggregateCapability` | interface | Base contract every capability implements: `attach(aggregate)` (required) + `detach?()` (optional). `SnapshotCapability`, `AuditCapability`, `VersioningCapability`, `EventSourcingCapability` all satisfy it |
76
+ | `IAggregateEventHandler<T>` | interface | Callable shape `(payload: T, metadata?) => void` — the handler function type registered via `registerEventHandler(type, fn)` |
77
+
78
+ > **BREAKING (v0.31.0):** `IAggregateBuilder<TId>` was removed — it was exported
79
+ > but shape-incompatible with the concrete `AggregateBuilder` class. Use
80
+ > `AggregateBuilder` directly for `withSnapshots()`, `withVersioning()`,
81
+ > `withEventSourcing()`, `withAudit()`, `withCustomCapability()`, `build()`.
66
82
 
67
83
  ### `AggregateRoot` method reference
68
84
 
@@ -136,6 +152,57 @@ if (hasAllCapabilities(order, [AuditCapability])) {
136
152
  }
137
153
  ```
138
154
 
155
+ ### Composing multiple capabilities and narrowing with `tryAs*` helpers
156
+
157
+ Build one aggregate with two capabilities (versioning + event sourcing), then
158
+ narrow it back to each capability-specific type with the non-throwing `tryAs*`
159
+ variants — useful in application-layer code where a missing capability should be
160
+ handled as a `Result`, not an exception.
161
+
162
+ ```typescript
163
+ import {
164
+ AggregateBuilder,
165
+ VersioningCapability,
166
+ EventSourcingCapability,
167
+ tryAsVersioningAggregate,
168
+ tryAsEventSourcingAggregate,
169
+ type AggregateWithVersioningCapability,
170
+ type AggregateWithEventSourcingCapability,
171
+ } from '@vytches/ddd-aggregates';
172
+ import { EntityId } from '@vytches/ddd-value-objects';
173
+
174
+ const order = AggregateBuilder.create({ id: EntityId.create() })
175
+ .withVersioning()
176
+ .withEventSourcing()
177
+ .build(Order);
178
+
179
+ // Narrow to versioning-capable: register an upcaster for an old event shape.
180
+ const versioningResult = tryAsVersioningAggregate(order);
181
+ if (versioningResult.isSuccess) {
182
+ const versioned: AggregateWithVersioningCapability<string> =
183
+ versioningResult.value;
184
+ versioned
185
+ .getCapability(VersioningCapability)!
186
+ .registerUpcaster('OrderCreated', 1, {
187
+ upcast: (payload: { amount: number }) => ({
188
+ ...payload,
189
+ currency: 'USD',
190
+ }),
191
+ });
192
+ }
193
+
194
+ // Narrow to event-sourcing-capable: persist to the configured event store.
195
+ const eventSourcingResult = tryAsEventSourcingAggregate(order);
196
+ if (eventSourcingResult.isSuccess) {
197
+ const sourced: AggregateWithEventSourcingCapability<string> =
198
+ eventSourcingResult.value;
199
+ await sourced.getCapability(EventSourcingCapability)!.saveToEventStore();
200
+ } else {
201
+ // eventSourcingResult.error is an AggregateError — handle without throwing.
202
+ console.error(eventSourcingResult.error.message);
203
+ }
204
+ ```
205
+
139
206
  ## Anti-Patterns
140
207
 
141
208
  **Mutating state directly instead of using `apply()`.** Assigning
package/README.md CHANGED
@@ -181,7 +181,11 @@ class OrderLine extends Entity<string> {
181
181
  | `IAggregateCapability` | interface | Base capability contract |
182
182
  | `IAggregateConstructorParams<TId>` | interface | Constructor parameter shape for aggregates |
183
183
  | `IAggregateEventHandler` | interface | Event handler registration contract |
184
- | `IAggregateBuilder` | interface | Builder contract |
184
+
185
+ > **BREAKING (v0.31.0):** `IAggregateBuilder` was removed. It was
186
+ > shape-incompatible with the concrete `AggregateBuilder` class (a broken public
187
+ > interface is worse than none). Use the `AggregateBuilder` class directly for
188
+ > the builder's fluent contract.
185
189
 
186
190
  ### Utility Types
187
191
 
@@ -1,4 +1,4 @@
1
- import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata, IEventStore, IEventUpcaster } from '@vytches/ddd-contracts';
1
+ import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata, IEventUpcaster } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Core interface for aggregate root functionality.
4
4
  * Provides the foundation for domain aggregates with event sourcing, versioning, and capability management.
@@ -80,6 +80,21 @@ export interface IAggregateConstructorParams<TId = string> {
80
80
  * Leave undefined for "no limit" (preserves backward compatibility).
81
81
  */
82
82
  maxEvents?: number;
83
+ /**
84
+ * VF-023 (D-4, AC6, non-breaking addition): controls what happens when
85
+ * `apply()` (live) or `loadFromHistory()` (replay) processes an event for
86
+ * which no handler was registered via `registerEventHandler()`.
87
+ *
88
+ * - `"warn"` (default): the event is still recorded/replayed, but a
89
+ * warning is logged via the library's internal logger. Preserves the
90
+ * previous behavior's intent (missing handlers did not throw) without
91
+ * silently swallowing the condition.
92
+ * - `"throw"`: fail fast instead — useful in tests or strict environments
93
+ * where a missing handler indicates a real bug.
94
+ *
95
+ * Leave undefined for the default `"warn"` behavior (backward compatible).
96
+ */
97
+ onMissingHandler?: 'warn' | 'throw';
83
98
  }
84
99
  /**
85
100
  * Base interface for all aggregate capabilities.
@@ -174,36 +189,6 @@ export interface IMiddlewareCapability extends IAggregateCapability {
174
189
  */
175
190
  use(middleware: EventAggregateMiddleware): this;
176
191
  }
177
- /**
178
- * Builder interface for creating aggregates with capabilities.
179
- * Provides fluent API for configuring aggregate features before construction.
180
- */
181
- export interface IAggregateBuilder<TId> {
182
- /**
183
- * Adds snapshot capability
184
- */
185
- withSnapshots<TState = unknown, TMeta = unknown>(): this;
186
- /**
187
- * Adds versioning capability
188
- */
189
- withVersioning(): this;
190
- /**
191
- * Adds event sourcing capability
192
- */
193
- withEventSourcing(eventStore?: IEventStore): this;
194
- /**
195
- * Adds audit capability
196
- */
197
- withAudit(): this;
198
- /**
199
- * Adds custom capability
200
- */
201
- withCustomCapability<T extends IAggregateCapability>(name: string, capability: T): this;
202
- /**
203
- * Builds the aggregate with all configured capabilities
204
- */
205
- build(): IAggregateRoot<TId>;
206
- }
207
192
  /**
208
193
  * Handler function for processing aggregate events.
209
194
  * Used internally by aggregates to handle domain events.
@@ -243,156 +228,3 @@ export type AggregateIdType<T> = T extends IAggregateRoot<infer TId> ? TId : nev
243
228
  export type AggregateWithCapabilities<TId, TCapabilities extends CapabilityName[]> = IAggregateRoot<TId> & {
244
229
  [K in TCapabilities[number] as `getCapability`]: (name: K) => K extends 'snapshot' ? ISnapshotCapability : K extends 'versioning' ? IVersioningCapability : K extends 'eventSourcing' ? IEventSourcingCapability : K extends 'audit' ? IAuditCapability : K extends 'middleware' ? IMiddlewareCapability : IAggregateCapability;
245
230
  };
246
- /**
247
- * Factory interface for creating and rebuilding aggregates.
248
- * Provides standard methods for aggregate lifecycle management.
249
- * @template TId - The type of the aggregate ID value (string, number, etc.)
250
- */
251
- export interface IAggregateFactory<TId = string, TAggregate extends IAggregateRoot<TId> = IAggregateRoot<TId>> {
252
- /**
253
- * Creates a new aggregate instance
254
- */
255
- create(id: EntityId<TId>, ...args: unknown[]): TAggregate;
256
- /**
257
- * Rebuilds aggregate from events
258
- */
259
- fromEvents(id: EntityId<TId>, events: IDomainEvent[]): TAggregate;
260
- /**
261
- * Rebuilds aggregate from snapshot and subsequent events
262
- */
263
- fromSnapshot<TState, TMeta>(snapshot: IAggregateSnapshot<TState, TMeta>, events: IDomainEvent[]): TAggregate;
264
- }
265
- /**
266
- * Interface for validating aggregate state and operations.
267
- * Enables business rule validation at the aggregate level.
268
- */
269
- export interface IAggregateValidator<TAggregate extends IAggregateRoot<unknown>> {
270
- /**
271
- * Validates aggregate state
272
- */
273
- validate(aggregate: TAggregate): ValidationResult;
274
- /**
275
- * Validates aggregate before applying event
276
- */
277
- validateBeforeEvent(aggregate: TAggregate, event: IDomainEvent): ValidationResult;
278
- }
279
- /**
280
- * Result of aggregate validation operation.
281
- * Contains validation status and any error details.
282
- */
283
- export interface ValidationResult {
284
- /** Whether the validation passed */
285
- isValid: boolean;
286
- /** List of validation errors if any */
287
- errors: IValidationError[];
288
- }
289
- /**
290
- * Represents a single validation error.
291
- * Provides details about what failed validation and why.
292
- */
293
- export interface IValidationError {
294
- /** The field or property that failed validation */
295
- field: string;
296
- /** Human-readable error message */
297
- message: string;
298
- /** Optional error code for programmatic handling */
299
- code?: string;
300
- }
301
- /**
302
- * Capability for caching aggregate data.
303
- * Provides key-value storage with TTL support for performance optimization.
304
- */
305
- export interface ICachingCapability extends IAggregateCapability {
306
- /**
307
- * Gets cached value
308
- */
309
- get<T>(key: string): T | undefined;
310
- /**
311
- * Sets cached value
312
- */
313
- set<T>(key: string, value: T, ttl?: number): void;
314
- /**
315
- * Clears all cached values
316
- */
317
- clear(): void;
318
- /**
319
- * Invalidates specific cached value
320
- */
321
- invalidate(key: string): void;
322
- }
323
- /**
324
- * Capability for collecting aggregate metrics.
325
- * Enables monitoring and observability of aggregate operations.
326
- */
327
- export interface IMetricsCapability extends IAggregateCapability {
328
- /**
329
- * Records a metric
330
- */
331
- record(name: string, value: number, tags?: Record<string, string>): void;
332
- /**
333
- * Increments a counter
334
- */
335
- increment(name: string, tags?: Record<string, string>): void;
336
- /**
337
- * Records timing information
338
- */
339
- timing(name: string, duration: number, tags?: Record<string, string>): void;
340
- /**
341
- * Gets recorded metrics
342
- */
343
- getMetrics(): MetricData[];
344
- }
345
- /**
346
- * Represents a single metric data point.
347
- * Contains metric information with optional tags and timestamp.
348
- */
349
- export interface MetricData {
350
- /** Metric name */
351
- name: string;
352
- /** Metric value */
353
- value: number;
354
- /** Type of metric */
355
- type: 'counter' | 'gauge' | 'timing';
356
- /** Optional tags for metric categorization */
357
- tags?: Record<string, string>;
358
- /** When the metric was recorded */
359
- timestamp: Date;
360
- }
361
- /**
362
- * Capability for security features like encryption and authorization.
363
- * Provides security controls for sensitive aggregate operations.
364
- */
365
- export interface ISecurityCapability extends IAggregateCapability {
366
- /**
367
- * Encrypts sensitive data
368
- */
369
- encrypt(data: unknown): string;
370
- /**
371
- * Decrypts sensitive data
372
- */
373
- decrypt(encryptedData: string): unknown;
374
- /**
375
- * Checks if user has permission to perform action
376
- */
377
- hasPermission(action: string, user?: unknown): boolean;
378
- /**
379
- * Logs security events
380
- */
381
- logSecurityEvent(event: SecurityEvent): void;
382
- }
383
- /**
384
- * Represents a security-related event.
385
- * Used for audit trails and security monitoring.
386
- */
387
- export interface SecurityEvent {
388
- /** Type of security event */
389
- type: 'access' | 'modification' | 'failure';
390
- /** Action that was performed */
391
- action: string;
392
- /** User who performed the action */
393
- user?: unknown;
394
- /** When the event occurred */
395
- timestamp: Date;
396
- /** Additional event metadata */
397
- metadata?: Record<string, unknown>;
398
- }
@@ -1,4 +1,4 @@
1
- import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '../../../contracts/src/index.ts';
1
+ import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that records every domain event applied to the aggregate as
4
4
  * an `IAuditEvent`. Useful for compliance, debugging, behavior analytics,
@@ -1,4 +1,4 @@
1
- import { IEventSourcingCapability, IEventStore, Capability } from '../../../contracts/src/index.ts';
1
+ import { IEventSourcingCapability, IEventStore, Capability } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that integrates an aggregate with an external `IEventStore` —
4
4
  * the persistence boundary for an event-sourced system. Allows the
@@ -1,4 +1,4 @@
1
- import { IAggregateSnapshot, ISnapshotCapability, Capability } from '../../../contracts/src/index.ts';
1
+ import { IAggregateSnapshot, ISnapshotCapability, Capability } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that enables snapshot creation and restoration on an aggregate
4
4
  * — a performance optimization for event-sourced aggregates with long
@@ -1,4 +1,4 @@
1
- import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '../../../contracts/src/index.ts';
1
+ import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '@vytches/ddd-contracts';
2
2
  import { IAggregateEventHandler } from '../aggregate-interfaces';
3
3
  /**
4
4
  * Capability that handles **event schema evolution** — when a stored event
@@ -63,7 +63,20 @@ export declare class VersioningCapability extends Capability<'versioning'> imple
63
63
  */
64
64
  registerUpcaster<TFrom = unknown, TTo = unknown>(eventType: string, sourceVersion: number, upcaster: IEventUpcaster<TFrom, TTo>): void;
65
65
  /**
66
- * @param {IDomainEvent} event - Event to process with version handling
66
+ * Applies registered upcasters to bring `event` from its stored version up
67
+ * to the aggregate's current version, then invokes the matching handler.
68
+ *
69
+ * **Upcasting only activates if `event.metadata.targetVersion` is set.**
70
+ * This is NOT inferred automatically from registered upcasters or from
71
+ * any aggregate-level "current version" — the caller (typically your
72
+ * event-sourcing replay/load path) must explicitly stamp each historical
73
+ * event's metadata with `targetVersion` before calling this method. If
74
+ * `targetVersion` is absent, `currentVersion` falls back to `eventVersion`
75
+ * (i.e. no upcasting happens — the event is passed through unchanged even
76
+ * if upcasters are registered for it).
77
+ *
78
+ * @param {IDomainEvent} event - Event to process with version handling;
79
+ * set `event.metadata.targetVersion` to enable upcasting
67
80
  * @param {Map<string, IAggregateEventHandler>} handlers - Map of event handlers
68
81
  */
69
82
  handleVersionedEvent(event: IDomainEvent, handlers: Map<string, IAggregateEventHandler>): void;
@@ -1,4 +1,4 @@
1
- import { Capability, IEventStore, EntityId } from '../../../contracts/src/index.ts';
1
+ import { Capability, IEventStore, EntityId } from '@vytches/ddd-contracts';
2
2
  import { IAggregateCapability, IAggregateConstructorParams } from '../aggregate-interfaces';
3
3
  import { AggregateRoot } from './aggregate-root';
4
4
  /**
@@ -1,5 +1,22 @@
1
1
  import { IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, IAggregateRoot } from '../aggregate-interfaces';
2
- import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '../../../contracts/src/index.ts';
2
+ import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '@vytches/ddd-contracts';
3
+ /**
4
+ * VF-023 (D-2, AC5, BREAKING, CRITICAL F-H4): module-private gating token
5
+ * for {@link AggregateRoot._internal_setState}. This is exported from this
6
+ * module (`./core/aggregate-root`) — NOT from the package's public barrel
7
+ * (`index.ts`) — so it is reachable only via a deep import within this
8
+ * package's own source (e.g. `SnapshotCapability`), never via
9
+ * `@vytches/ddd-aggregates`. Consumers cannot obtain this token, and
10
+ * therefore cannot call `_internal_setState` at all: TypeScript rejects the
11
+ * call without it, and even a `as any` cast would need the exact symbol
12
+ * reference, which is not exported publicly.
13
+ *
14
+ * Previously `_internal_setState` was a plain public method reachable via
15
+ * any `as any`/duck-typed cast — no compile-time or run-time barrier
16
+ * prevented domain code from bypassing `apply()`/`loadFromHistory()` and
17
+ * corrupting the version/event-log invariant directly.
18
+ */
19
+ export declare const INTERNAL_STATE_TOKEN: unique symbol;
3
20
  /**
4
21
  * Base class for aggregate roots — the *transactional consistency boundary*
5
22
  * of a Domain-Driven Design model. Per Eric Evans (Blue Book, ch. 6):
@@ -12,7 +29,7 @@ import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetada
12
29
  *
13
30
  * `AggregateRoot<TId>` provides:
14
31
  *
15
- * - **Identity** via `EntityId<TId>` (`getId()`)
32
+ * - **Identity** via `EntityId<TId>` (`getId()`, `equals()`)
16
33
  * - **Versioning** for optimistic locking (`getVersion()`,
17
34
  * `getInitialVersion()`)
18
35
  * - **Event emission** via `apply()` — append a `DomainEvent`, increment
@@ -111,28 +128,69 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
111
128
  * loops or malicious replay payloads.
112
129
  */
113
130
  private readonly _maxEvents;
131
+ /**
132
+ * VF-023 (D-4, AC6, non-breaking addition): controls what happens when
133
+ * `apply()` or `loadFromHistory()` processes an event for which no
134
+ * handler was registered via `registerEventHandler()`. Defaults to
135
+ * `"warn"` (preserves the previous silent-but-logged-nowhere behavior's
136
+ * intent without a breaking default) — the event is still recorded /
137
+ * replayed, but a warning is logged via the library's internal logger.
138
+ * Set to `"throw"` to fail fast instead, useful in tests or strict
139
+ * environments where a missing handler indicates a real bug.
140
+ */
141
+ private readonly _onMissingHandler;
114
142
  constructor(params: IAggregateConstructorParams<TId>);
115
143
  /**
116
144
  * Get the unique identifier of this aggregate
117
145
  */
118
146
  getId(): EntityId<TId>;
119
147
  /**
148
+ * Identity-based equality, matching {@link Entity.equals}. Two aggregates
149
+ * are equal iff they have the same id (compared via `EntityId.equals()`);
150
+ * attribute/version differences are ignored.
151
+ *
152
+ * VF-023 (D-5, AC10, non-breaking addition): `AggregateRoot` does not
153
+ * extend `Entity` (it implements `IAggregateRoot<TId>` directly), so this
154
+ * mirrors `Entity.equals()` rather than inheriting it.
155
+ *
156
+ * @param other - Aggregate to compare against. May be `null` or `undefined`.
157
+ * @returns `true` if both aggregates have the same id; `false` otherwise.
158
+ */
159
+ equals(other: AggregateRoot<TId> | null | undefined): boolean;
160
+ /**
161
+ * The version increments by one each time `apply()` successfully records a
162
+ * domain event; compare it against the persisted version to detect
163
+ * concurrent modification (optimistic locking).
120
164
  *
121
165
  * @returns {number} Current version number for optimistic locking
122
166
  */
123
167
  getVersion(): number;
124
168
  /**
169
+ * The version this aggregate had when it was loaded (or as of the last
170
+ * `commit()`), letting callers work out how many events have been applied
171
+ * since without recomputing from the event log.
125
172
  *
126
173
  * @returns {number} Initial version when aggregate was loaded
127
174
  */
128
175
  getInitialVersion(): number;
129
176
  /**
177
+ * Whether this aggregate has recorded domain events since its last
178
+ * `commit()` — use it to decide whether persistence/dispatch work is
179
+ * needed at all.
130
180
  *
131
181
  * @returns {boolean} True if aggregate has uncommitted domain events
132
182
  */
133
183
  hasChanges(): boolean;
134
184
  /**
135
- *
185
+ * VF-023 (D-3, AC11, BREAKING): the returned array and every event object
186
+ * it contains (including nested `payload`/`metadata`) are now deep-frozen
187
+ * via `LibUtils.deepFreeze` — the same immutability guarantee applied to
188
+ * `BaseValueObject` values (see `base-value-object.ts`). Previously this
189
+ * returned only a shallow array copy: the array itself couldn't be
190
+ * mutated by callers, but the event objects (and their nested payload
191
+ * data) inside it could still be mutated in place, silently corrupting
192
+ * state shared with `_domainEvents` (the same object references are
193
+ * stored internally — events are never re-cloned after creation).
136
194
  * @returns {ReadonlyArray<IDomainEvent>} Array of uncommitted domain events
137
195
  */
138
196
  getDomainEvents(): ReadonlyArray<IDomainEvent>;
@@ -151,9 +209,28 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
151
209
  * @param {string | IDomainEvent<P>} eventTypeOrEvent - Event type string or complete event object
152
210
  * @param {P} payload - Event payload data
153
211
  * @param {Partial<IEventMetadata>} metadata - Optional event metadata
212
+ * @throws {Error} when the aggregate's configured maxEvents limit would be exceeded
213
+ * @example Hitting the maxEvents guard
214
+ * ```typescript
215
+ * class Order extends AggregateRoot<string> {
216
+ * record(customerId: string, amount: number): void {
217
+ * this.apply('OrderCreated', { customerId, amount });
218
+ * }
219
+ * }
220
+ *
221
+ * const order = new Order({ id: EntityId.create(), version: 0, maxEvents: 1 });
222
+ * order.record('c-1', 100); // ok, 1 event recorded
223
+ * order.record('c-1', 100); // throws: maxEvents limit of 1 exceeded
224
+ * ```
154
225
  */
155
226
  protected apply<P = unknown>(eventTypeOrEvent: string | IDomainEvent<P>, payload?: P, metadata?: Partial<IEventMetadata>): void;
156
227
  private handleEvent;
228
+ /**
229
+ * VF-023 (D-4, AC6): called from `handleEvent()`, which is shared by both
230
+ * `apply()` (live) and `loadFromHistory()` (replay) — so the missing-
231
+ * handler policy applies identically to both paths.
232
+ */
233
+ private reportMissingHandler;
157
234
  /**
158
235
  * Reconstruct aggregate state from historical domain events
159
236
  * @param {IDomainEvent[]} events - Array of events to replay for state reconstruction
@@ -190,10 +267,20 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
190
267
  getCapabilityTypes(): string[];
191
268
  private static sanitizeMetadata;
192
269
  /**
193
- * Internal method for capabilities to access private state
194
- * Should only be used by capabilities, not directly by domain code
270
+ * Internal method for capabilities to access private state.
271
+ *
272
+ * VF-023 (D-2, AC5, BREAKING, CRITICAL F-H4): gated by
273
+ * {@link INTERNAL_STATE_TOKEN}, a module-private `unique symbol` that is
274
+ * exported only from this module (not from the package's public barrel).
275
+ * Callers must supply the exact token reference to invoke this method —
276
+ * consumers of `@vytches/ddd-aggregates` cannot obtain it, so this method
277
+ * is unreachable from outside this package's own source, even via
278
+ * `as unknown as {...}` casts (the token value itself, not just its type,
279
+ * is required and cannot be forged).
280
+ *
281
+ * @throws {TypeError} if `token` does not match {@link INTERNAL_STATE_TOKEN}
195
282
  */
196
- _internal_setState(state: {
283
+ _internal_setState(token: typeof INTERNAL_STATE_TOKEN, state: {
197
284
  version: number;
198
285
  initialVersion: number;
199
286
  domainEvents: IDomainEvent[];
@@ -1,5 +1,5 @@
1
- import { Result } from '../../../utils/src/index.ts';
2
- import { CapabilityConstructor, Capability, IAggregateCapability, IAggregateSnapshot } from '../../../contracts/src/index.ts';
1
+ import { Result } from '@vytches/ddd-utils';
2
+ import { CapabilityConstructor, Capability, IAggregateCapability, IAggregateSnapshot } from '@vytches/ddd-contracts';
3
3
  import { AggregateError } from '../aggregate-errors';
4
4
  import { SnapshotCapability } from '../capabilities/snapshot-capability';
5
5
  import { VersioningCapability } from '../capabilities/versioning-capability';
@@ -1,4 +1,4 @@
1
- import { EntityId } from '../../../contracts/src/index.ts';
1
+ import { EntityId } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Base class for domain entities — objects with identity that may be
4
4
  * referenced from other aggregates but are NOT themselves aggregate roots.