tshex-cli 1.0.24 → 1.0.26

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.
@@ -1,94 +1,64 @@
1
1
  ### HTTP
2
2
 
3
- The HTTP contracts define a transport-facing boundary without coupling the
4
- generated structure to a specific framework.
3
+ The HTTP contracts define a framework-agnostic, transport-facing boundary.
5
4
  They are used when an adapter needs to describe requests, responses, handlers,
6
5
  or middleware in a consistent way.
7
6
 
8
- The generated template keeps `HttpRequest` and `HttpResponse` empty on purpose.
9
- Each project can extend them with the fields required by its own transport.
7
+ The generated template relies on the standard `Request` and `Response` types
8
+ from the Fetch API, so adapters work directly with the platform's own APIs.
10
9
 
11
- #### Response Body
10
+ The concern is split across four files under `shared/application/http/`:
12
11
 
13
- `HttpResponseBody` is responsible for standardizing the shape of the response
14
- payload.
12
+ 1. `http.ts` for the request/response boundary contracts and `HttpError`;
13
+ 2. `json-api.ts` for a type-only JSON:API v1.1 specification;
14
+ 3. `json-web-token.ts` for a type-only JOSE/JWT specification;
15
+ 4. `opengraph.ts` for a type-only Open Graph and social metadata specification.
15
16
 
16
- ```ts title="shared/application/http.ts"
17
- export interface HttpResponseBody {
18
- readonly data: Record<string, unknown> | null
19
- readonly errors: string[] | null
20
- readonly links: Record<string, URL> | null
21
- }
22
- ```
23
-
24
- This structure makes successful data, error messages, and related links
25
- explicit without forcing a specific router or server implementation.
17
+ Only `http.ts` contains runtime code. The other three files describe
18
+ compile-time structure for widely used formats so adapters do not have to
19
+ redefine them; they do not implement parsing, validation, or serialization.
26
20
 
27
21
  #### Request Handler
28
22
 
29
23
  `HttpRequestHandler` is responsible for processing a request and returning a
30
24
  response.
31
25
 
32
- ```ts title="shared/application/http.ts"
26
+ ```ts title="shared/application/http/http.ts"
33
27
  export interface HttpRequestHandler {
34
- handle(request: HttpRequest): HttpResponse | Promise<HttpResponse>
28
+ handle(request: Request): Response | Promise<Response>
35
29
  }
36
30
  ```
37
31
 
38
- In the following example we define adapter-specific request and response types,
39
- then implement a handler.
32
+ In the following example we implement a handler using the standard `Request`
33
+ and `Response` objects.
40
34
 
41
35
  ```ts title="users/adapters/get-user-handler.ts"
42
- import {
43
- HttpRequest,
44
- HttpRequestHandler,
45
- HttpResponse,
46
- HttpResponseBody,
47
- } from '../../shared/application/http.js'
48
-
49
- interface UserHttpRequest extends HttpRequest {
50
- readonly params: {
51
- id: string
52
- }
53
- }
54
-
55
- interface UserHttpResponse extends HttpResponse {
56
- readonly status: number
57
- readonly body: HttpResponseBody
58
- }
36
+ import { HttpRequestHandler } from '../../shared/application/http/http.js'
59
37
 
60
38
  export class GetUserHandler implements HttpRequestHandler {
61
- public handle(request: HttpRequest): HttpResponse {
62
- const typedRequest = request as UserHttpRequest
63
-
64
- return {
65
- status: 200,
66
- body: {
67
- data: {
68
- id: typedRequest.params.id,
69
- },
70
- errors: null,
71
- links: null,
72
- },
73
- } as UserHttpResponse
39
+ public handle(request: Request): Response {
40
+ const id = new URL(request.url).pathname.split('/').at(-1)
41
+
42
+ return Response.json({ data: { id } }, { status: 200 })
74
43
  }
75
44
  }
76
45
  ```
77
46
 
78
- The generated `HttpRequest` and `HttpResponse` interfaces stay empty, so the
79
- adapter declares the transport-specific fields locally. This keeps the shared
80
- contract small and portable.
47
+ Using the standard `Request` and `Response` types means the adapter relies
48
+ on the platform's own APIs, such as `request.url`, `request.headers`, and
49
+ `Response.json`. The shape of the JSON body itself, when the adapter follows
50
+ JSON:API, is described by the document types in `json-api.ts`.
81
51
 
82
52
  #### Middleware
83
53
 
84
54
  `HttpMiddleware` is responsible for running logic before or around the handler.
85
55
 
86
- ```ts title="shared/application/http.ts"
56
+ ```ts title="shared/application/http/http.ts"
87
57
  export interface HttpMiddleware {
88
58
  process(
89
- request: HttpRequest,
59
+ request: Request,
90
60
  handler: HttpRequestHandler,
91
- ): HttpResponse | Promise<HttpResponse>
61
+ ): Response | Promise<Response>
92
62
  }
93
63
  ```
94
64
 
@@ -97,29 +67,69 @@ Now that the handler exists, middleware can wrap it.
97
67
  ```ts title="users/adapters/request-logger.ts"
98
68
  import {
99
69
  HttpMiddleware,
100
- HttpRequest,
101
70
  HttpRequestHandler,
102
- HttpResponse,
103
- } from '../../shared/application/http.js'
71
+ } from '../../shared/application/http/http.js'
104
72
 
105
73
  export class RequestLoggerMiddleware implements HttpMiddleware {
106
74
  public async process(
107
- request: HttpRequest,
75
+ request: Request,
108
76
  handler: HttpRequestHandler,
109
- ): Promise<HttpResponse> {
77
+ ): Promise<Response> {
110
78
  void request
111
79
  return handler.handle(request)
112
80
  }
113
81
  }
114
82
  ```
115
83
 
116
- This middleware does not mutate the request or response. It only shows where
84
+ This middleware passes the request through unchanged, showing where
117
85
  cross-cutting behavior belongs in the generated HTTP abstraction.
118
86
 
119
- > **Warning**
120
- > Do not treat the shared HTTP contracts as a full framework abstraction. They
121
- > only define the minimum boundary for adapters. Routing, serialization, and
122
- > status code policies remain the responsibility of the concrete transport.
87
+ #### HTTP Errors
88
+
89
+ `HttpError` is responsible for carrying an HTTP status code alongside a
90
+ matching message.
91
+
92
+ ```ts title="shared/application/http/http.ts"
93
+ export class HttpError extends Error {
94
+ public static readonly messages: { [code: number]: string } = Object.freeze({
95
+ 400: 'Bad Request',
96
+ 404: 'Not Found',
97
+ 409: 'Conflict',
98
+ // ...remaining standard 4xx/5xx status codes
99
+ 500: 'Internal Server Error',
100
+ })
101
+
102
+ public readonly code: number
103
+
104
+ constructor(code: number, message?: string) {
105
+ super(message ?? HttpError.messages[code] ?? 'Unknown Error')
106
+ this.code = code
107
+ this.name = 'HttpError'
108
+ }
109
+ }
110
+ ```
111
+
112
+ `HttpError.messages` maps every standard 4xx/5xx status code to its reason
113
+ phrase. A caller can throw `new HttpError(404)` to get the standard message for
114
+ free, or pass an explicit `message` to override it. Codes outside the map fall
115
+ back to `'Unknown Error'`.
116
+
117
+ ```ts title="users/adapters/get-user-handler.ts"
118
+ import { HttpError } from '../../shared/application/http/http.js'
119
+
120
+ function assertFound<T>(value: T | null): T {
121
+ if (value === null) {
122
+ throw new HttpError(404)
123
+ }
124
+
125
+ return value
126
+ }
127
+ ```
128
+
129
+ > **Note**
130
+ > The shared HTTP contracts define the minimum boundary for adapters. Routing,
131
+ > serialization, and status code policies beyond `HttpError` are the
132
+ > responsibility of the concrete transport.
123
133
 
124
134
  #### Example Flow
125
135
 
@@ -132,3 +142,142 @@ flowchart LR
132
142
 
133
143
  This flow keeps the transport boundary explicit while leaving framework choices
134
144
  to the adapter layer.
145
+
146
+ #### JSON:API
147
+
148
+ `json-api.ts` declares a type-only implementation of the
149
+ [JSON:API v1.1](https://jsonapi.org/format/) specification, including the
150
+ [Atomic Operations extension](https://jsonapi.org/ext/atomic/). It gives an
151
+ adapter a shared vocabulary for request and response bodies without forcing a
152
+ particular server framework.
153
+
154
+ The main building blocks are:
155
+
156
+ - `JsonApiResourceObject` / `JsonApiResourceIdentifier` for resources and
157
+ resource linkage, generic over the resource `type`, `attributes`, and
158
+ `relationships`;
159
+ - `JsonApiRelationship`, `JsonApiToOneRelationship`, and
160
+ `JsonApiToManyRelationship` for relationship objects;
161
+ - `JsonApiError` for the top-level error object;
162
+ - `JsonApiDocument` (and its narrower aliases such as
163
+ `JsonApiSingleResourceDocument` and `JsonApiResourceCollectionDocument`) for
164
+ the top-level document, discriminated between a data document, an error
165
+ document, and a meta-only document;
166
+ - `JsonApiAtomicOperationsDocument` / `JsonApiAtomicResultsDocument` for the
167
+ Atomic Operations extension request and response bodies.
168
+
169
+ ```ts title="users/adapters/get-user-handler.ts"
170
+ import { HttpRequestHandler } from '../../shared/application/http/http.js'
171
+ import {
172
+ JsonApiSingleResourceDocument,
173
+ JsonApiResourceObject,
174
+ } from '../../shared/application/http/json-api.js'
175
+
176
+ type UserAttributes = { email: string }
177
+ type UserResource = JsonApiResourceObject<'users', UserAttributes>
178
+
179
+ export class GetUserHandler implements HttpRequestHandler {
180
+ public handle(request: Request): Response {
181
+ const id = new URL(request.url).pathname.split('/').at(-1) ?? ''
182
+
183
+ const body: JsonApiSingleResourceDocument<UserResource> = {
184
+ data: {
185
+ type: 'users',
186
+ id,
187
+ attributes: { email: 'ada@example.com' },
188
+ },
189
+ }
190
+
191
+ return Response.json(body, { status: 200 })
192
+ }
193
+ }
194
+ ```
195
+
196
+ > **Hint**
197
+ > These declarations only provide compile-time structure. Rules that depend on
198
+ > runtime values, URI validity, document-wide uniqueness, or member-name
199
+ > character validation still require explicit checks in the adapter.
200
+
201
+ #### JSON Web Tokens
202
+
203
+ `json-web-token.ts` declares a type-only implementation of the JOSE and JWT
204
+ family of RFCs (JWS, JWE, JWK, JWT, and related extensions such as DPoP and
205
+ selective disclosure). It lets an adapter describe tokens and keys precisely
206
+ without depending on a specific JOSE library's own types.
207
+
208
+ The main building blocks are:
209
+
210
+ - branded wire-format primitives such as `Base64Url`, `NumericDate`, and
211
+ `CompactJwt`;
212
+ - `JsonWebKey` / `JsonWebKeySet` for keys, covering EC, RSA, `oct`, OKP, and
213
+ ML-DSA (`AKP`) key types;
214
+ - `JwsHeader` / `JweHeader` for protected header parameters, and
215
+ `JwsJsonSerialization` / `JweJsonSerialization` for the JSON serializations;
216
+ - `JwtClaims` (built on `IanaRegisteredJwtClaims`) for decoded payloads, plus
217
+ ready-made profiles such as `OpenIdConnectIdTokenClaims`,
218
+ `OAuth2JwtAccessTokenClaims`, `DpopProofClaims`, and `SdJwtClaims`;
219
+ - service contracts an adapter can implement against a concrete JOSE
220
+ library: `JwtDecoder`, `JwsSigner`, `JwsVerifier`, `JweEncrypter`,
221
+ `JweDecrypter`, `JwkThumbprinter`, and `JwksResolver`, plus
222
+ `JwtValidationResult` for the outcome of validating a token against a
223
+ `JwtValidationPolicy`.
224
+
225
+ ```ts title="users/adapters/verify-access-token.ts"
226
+ import {
227
+ JwsVerifier,
228
+ OAuth2JwtAccessTokenClaims,
229
+ CompactJws,
230
+ JsonWebKey,
231
+ } from '../../shared/application/http/json-web-token.js'
232
+
233
+ export function verifyAccessToken(
234
+ verifier: JwsVerifier,
235
+ token: CompactJws,
236
+ key: JsonWebKey,
237
+ ) {
238
+ return verifier.verify<OAuth2JwtAccessTokenClaims>(token, key)
239
+ }
240
+ ```
241
+
242
+ > **Hint**
243
+ > This module has no runtime implementation. Pair it with a concrete JOSE
244
+ > library (for signing, encryption, or verification) and use these types to
245
+ > annotate its inputs and outputs.
246
+
247
+ #### Open Graph
248
+
249
+ `opengraph.ts` declares a type-only implementation of the
250
+ [Open Graph protocol](https://ogp.me/), the Twitter Card meta tags, and
251
+ Facebook's compatibility extensions. It is used when an adapter needs to build
252
+ or read the social-sharing metadata of a page.
253
+
254
+ The main building blocks are:
255
+
256
+ - `OpenGraphMetadata`, a union of every standard Open Graph object type
257
+ (`OpenGraphWebsite`, `OpenGraphArticle`, `OpenGraphBook`, `OpenGraphProfile`,
258
+ the `music.*` and `video.*` types, `OpenGraphPaymentLink`, and
259
+ `OpenGraphCustomObject` for CURIE-style custom types);
260
+ - `OpenGraphMetaTag` and `TwitterMetaTag`, the flat `property`/`content` and
261
+ `name`/`content` tag representations closer to the actual `<meta>` markup;
262
+ - `SocialMetadataDocument`, an aggregate of Open Graph, Twitter, Facebook, and
263
+ standard head metadata for a single page, and `RawSocialMetadataDocument`
264
+ for its rendered, tag-list form.
265
+
266
+ ```ts title="users/adapters/user-profile-metadata.ts"
267
+ import { OpenGraphProfile } from '../../shared/application/http/opengraph.js'
268
+
269
+ export function buildProfileMetadata(username: string): OpenGraphProfile {
270
+ return {
271
+ type: 'profile',
272
+ title: username,
273
+ url: `https://example.com/users/${username}`,
274
+ images: [{ url: `https://example.com/users/${username}/avatar.png` }],
275
+ username,
276
+ }
277
+ }
278
+ ```
279
+
280
+ > **Hint**
281
+ > This module has no runtime implementation, including no HTML rendering. Use
282
+ > `OpenGraphMetadata` to build the data and a separate template or renderer to
283
+ > emit the `<meta>` tags described by `OpenGraphMetaTag`/`TwitterMetaTag`.
@@ -28,22 +28,57 @@ any adapter to use a particular logger implementation.
28
28
  `Logger` is responsible for receiving log data from the application layer.
29
29
 
30
30
  ```ts title="shared/application/loggers.ts"
31
+ import { type TimeZone } from '../../types/iana.js'
32
+ import { type Locale } from '../../types/cldr.js'
33
+
31
34
  export abstract class Logger {
32
- public abstract debug(data: unknown): void
35
+ [property: string]: unknown
33
36
 
34
- public abstract info(data: unknown): void
37
+ public name: string = 'main'
35
38
 
36
- public abstract warning(data: unknown): void
39
+ public level: number = 0
37
40
 
38
- public abstract error(data: unknown): void
41
+ public datetimeLocales: Locale[] = ['en-GB']
39
42
 
40
- public abstract critical(data: unknown): void
41
- }
43
+ public datetimeFormatOptions: Intl.DateTimeFormatOptions & { timeZone: TimeZone } = {
44
+ timeZone: 'UTC',
45
+ year: 'numeric',
46
+ month: '2-digit',
47
+ day: '2-digit',
48
+ hour: '2-digit',
49
+ minute: '2-digit',
50
+ second: '2-digit',
51
+ fractionalSecondDigits: 3,
52
+ hourCycle: 'h23'
53
+ }
54
+
55
+ public abstract debug(data: unknown): void
56
+
57
+ public abstract info(data: unknown): void
58
+
59
+ public abstract warning(data: unknown): void
60
+
61
+ public abstract error(data: unknown): void
62
+
63
+ public abstract critical(data: unknown): void
64
+
65
+ protected getCurrentDatetime(): string {
66
+ return new Date().toLocaleString(this.datetimeLocales, this.datetimeFormatOptions)
67
+ }
68
+ } //:: class
42
69
  ```
43
70
 
44
71
  The contract is intentionally small. It defines the actions the application can
45
72
  request, while the adapter decides how those actions are persisted or displayed.
46
73
 
74
+ `name` and `level` identify the logger instance and its minimum severity, so an
75
+ adapter can decide which logs to emit or route. `datetimeLocales` and
76
+ `datetimeFormatOptions` control how `getCurrentDatetime()` formats the current
77
+ moment, using the `Locale` type from `types/cldr.d.ts` and the `TimeZone` type
78
+ from `types/iana.d.ts`. Adapters can use `getCurrentDatetime()` to timestamp
79
+ log entries consistently, regardless of the runtime environment's own locale
80
+ or timezone.
81
+
47
82
  #### First Adapter
48
83
 
49
84
  In the following example we implement a console-based logger.
@@ -51,43 +86,32 @@ In the following example we implement a console-based logger.
51
86
  ```ts title="users/adapters/console-logger.ts"
52
87
  import { Logger } from '../../shared/application/loggers.js'
53
88
 
54
- type ExternalService = {
55
- debug(data: unknown): void
56
- info(data: unknown): void
57
- warning(data: unknown): void
58
- error(data: unknown): void
59
- critical(data: unknown): void
60
- }
61
-
62
89
  export class ConsoleLogger extends Logger {
63
- constructor(protected readonly externalService: ExternalService) {
64
- super()
65
- }
66
-
67
90
  public debug(data: unknown): void {
68
- this.externalService.debug(data)
91
+ console.debug(this.getCurrentDatetime(), this.name, data)
69
92
  }
70
93
 
71
94
  public info(data: unknown): void {
72
- this.externalService.info(data)
95
+ console.info(this.getCurrentDatetime(), this.name, data)
73
96
  }
74
97
 
75
98
  public warning(data: unknown): void {
76
- this.externalService.warning(data)
99
+ console.warn(this.getCurrentDatetime(), this.name, data)
77
100
  }
78
101
 
79
102
  public error(data: unknown): void {
80
- this.externalService.error(data)
103
+ console.error(this.getCurrentDatetime(), this.name, data)
81
104
  }
82
105
 
83
106
  public critical(data: unknown): void {
84
- this.externalService.critical(data)
107
+ console.error(this.getCurrentDatetime(), this.name, data)
85
108
  }
86
109
  }
87
110
  ```
88
111
 
89
112
  This adapter satisfies the generated contract without changing the application
90
- layer.
113
+ layer. It reuses `getCurrentDatetime()` to prefix every entry with a
114
+ consistently formatted timestamp.
91
115
 
92
116
  #### Service Integration
93
117
 
@@ -1,9 +1,8 @@
1
1
  ### Aggregates
2
2
 
3
- An aggregate is responsible for grouping multiple entities into one logical
4
- domain unit.
5
- It is used when the behavior of the concept depends on the collaboration of
6
- several entities instead of a single entity in isolation.
3
+ An aggregate groups multiple entities into one logical domain unit.
4
+ It is useful when the behavior of a concept depends on the collaboration of
5
+ several entities working together.
7
6
 
8
7
  The generated template provides `Aggregate` as a semantic base class.
9
8
 
@@ -15,45 +14,145 @@ The generated template provides `Aggregate` as a semantic base class.
15
14
  export abstract class Aggregate {}
16
15
  ```
17
16
 
18
- This means the generated base class does not impose persistence rules, event
19
- publication, or identity behavior. Those concerns remain in the concrete domain
20
- model of the project.
17
+ The class is intentionally empty. Its role is to provide the semantic base for
18
+ an aggregate whose concrete implementation gathers the involved entities as a
19
+ key-value structure through its own properties, such as `buyer`, `seller`,
20
+ `order`, or `invoice`. Persistence rules, event publication, and identity
21
+ behavior live in the concrete domain model of the project.
21
22
 
22
23
  #### First Aggregate
23
24
 
24
- In the following example we model a shopping cart as a group of line items.
25
+ In the following example we model a sale as one domain unit composed of a
26
+ buyer, a seller, an order, and an invoice generated by the aggregate itself.
25
27
 
26
- ```ts title="users/domain/cart.ts"
28
+ ```ts title="sales/domain/sale.ts"
27
29
  import { Aggregate } from '../../shared/domain/aggregates.js'
28
30
  import { Entity } from '../../shared/domain/entities.js'
29
31
 
30
- class LineItem extends Entity {
32
+ class Person extends Entity {
33
+ public readonly id: string
34
+ public readonly name: string
35
+
36
+ public constructor(id: string, name: string) {
37
+ super()
38
+ this.id = id
39
+ this.name = name
40
+ }
41
+
42
+ public equals(other: Entity): boolean {
43
+ return other instanceof Person && this.id === other.id
44
+ }
45
+ }
46
+
47
+ class Product extends Entity {
48
+ public readonly id: string
49
+ public readonly name: string
50
+ public readonly price: number
51
+
52
+ public constructor(id: string, name: string, price: number) {
53
+ super()
54
+ this.id = id
55
+ this.name = name
56
+ this.price = price
57
+ }
58
+
59
+ public equals(other: Entity): boolean {
60
+ return other instanceof Product && this.id === other.id
61
+ }
62
+ }
63
+
64
+ class Order extends Entity {
65
+ public readonly id: string
66
+ public readonly products: Product[]
67
+
68
+ public constructor(id: string, products: Product[]) {
69
+ super()
70
+ this.id = id
71
+ this.products = products
72
+ }
73
+
74
+ public equals(other: Entity): boolean {
75
+ return other instanceof Order && this.id === other.id
76
+ }
77
+ }
78
+
79
+ class Invoice extends Entity {
80
+ public readonly id: string
81
+ public readonly order: Order
82
+ public readonly buyer: Person
83
+ public readonly seller: Person
84
+ public readonly total: number
85
+
31
86
  public constructor(
32
- public readonly id: string,
33
- public readonly quantity: number,
87
+ id: string,
88
+ order: Order,
89
+ buyer: Person,
90
+ seller: Person,
34
91
  ) {
35
92
  super()
93
+ this.id = id
94
+ this.order = order
95
+ this.buyer = buyer
96
+ this.seller = seller
97
+ this.total = this.order.products.reduce(
98
+ (sum, product) => sum + product.price,
99
+ 0,
100
+ )
36
101
  }
37
102
 
38
103
  public equals(other: Entity): boolean {
39
- return other instanceof LineItem && this.id === other.id
104
+ return other instanceof Invoice && this.id === other.id
40
105
  }
41
106
  }
42
107
 
43
- export class Cart extends Aggregate {
44
- public constructor(public readonly items: Array<LineItem>) {
108
+ export class Sale extends Aggregate {
109
+ public readonly buyer: Person
110
+ public readonly seller: Person
111
+ public readonly order: Order
112
+ protected invoice: Invoice | null
113
+
114
+ public constructor(
115
+ buyer: Person,
116
+ seller: Person,
117
+ order: Order,
118
+ ) {
45
119
  super()
120
+ this.buyer = buyer
121
+ this.seller = seller
122
+ this.order = order
123
+ this.invoice = null
124
+ }
125
+
126
+ public generateInvoice(): { invoice: Invoice } {
127
+ const id = Math.random().toString()
128
+ const invoice = new Invoice(
129
+ id,
130
+ this.order,
131
+ this.buyer,
132
+ this.seller,
133
+ )
134
+
135
+ this.invoice = invoice
136
+
137
+ return { invoice }
46
138
  }
47
139
 
48
- public totalItems(): number {
49
- return this.items.reduce((sum, item) => sum + item.quantity, 0)
140
+ public getInvoice(): Invoice | null {
141
+ return this.invoice
50
142
  }
51
143
  }
52
144
  ```
53
145
 
54
- `Cart` is an aggregate because its behavior depends on the collection of
55
- entities that compose it. `LineItem` remains an entity because its identity is
56
- independent inside the aggregate.
146
+ `Sale` is an aggregate because the rule for generating the invoice depends on
147
+ the collaboration between buyer, seller, and order. In this example, both
148
+ buyer and seller are modeled with the same `Person` entity, while `Order`
149
+ stores the list of purchased products and `Invoice` stores the computed
150
+ `total`. `Invoice` is created inside the aggregate, and its constructor
151
+ calculates the total from the order products when the invoice is generated. The
152
+ `invoice` field stays inside the aggregate lifecycle and `getInvoice()` offers
153
+ controlled access to the generated invoice. When a domain object already
154
+ exists, the example uses that object directly and keeps the collaboration
155
+ between domain parts explicit.
57
156
 
58
157
  #### Responsibility Boundary
59
158
 
@@ -66,14 +165,15 @@ Examples include:
66
165
  2. keeping related entities in a consistent state;
67
166
  3. exposing operations that depend on the collaboration of those entities.
68
167
 
69
- The generated `Aggregate` base class does not implement these rules for you. It
70
- only provides the semantic place where those rules belong.
168
+ The generated `Aggregate` base class provides the semantic place where those
169
+ rules belong. The concrete aggregate defines and coordinates the rules that
170
+ keep the domain unit consistent.
71
171
 
72
172
  > **Warning**
73
- > Do not extend `Aggregate` only to create a container for unrelated values. Use
74
- > it when the concept represents one domain unit composed of several parts.
173
+ > Extend `Aggregate` when the concept represents one domain unit composed of
174
+ > several related parts and shared rules.
75
175
 
76
176
  #### Next Step
77
177
 
78
178
  Aggregates usually collaborate with entities and value objects. The base entity
79
- behavior is documented in `entities.md`.
179
+ behavior is documented in `entities.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -45,6 +45,7 @@
45
45
  "commander": "^12.1.0"
46
46
  },
47
47
  "devDependencies": {
48
+ "@fission-ai/openspec": "^1.6.0",
48
49
  "@types/node": "^22.5.4",
49
50
  "typescript": "^5.4.5"
50
51
  },