tshex-cli 1.0.23 → 1.0.25

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/build/main.js CHANGED
@@ -45,7 +45,7 @@ function executeCreateReactContext(templatesDir, contextDir) {
45
45
  console.error(err);
46
46
  }
47
47
  }
48
- function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir) {
48
+ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir, rootSourceDir = sourceDir) {
49
49
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
50
50
  if (fs.existsSync(destinationDir) === false) {
51
51
  fs.mkdirSync(destinationDir, { recursive: true });
@@ -54,7 +54,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
54
54
  const sourcePath = path.join(sourceDir, entry.name);
55
55
  const destinationPath = path.join(destinationDir, entry.name);
56
56
  if (entry.isDirectory()) {
57
- if (entry.name === 'shared') {
57
+ if (entry.name === 'shared' && sourceDir === rootSourceDir) {
58
58
  continue;
59
59
  }
60
60
  if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
@@ -63,7 +63,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
63
63
  if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
64
64
  continue;
65
65
  }
66
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir);
66
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir);
67
67
  continue;
68
68
  }
69
69
  if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
@@ -1,12 +1,17 @@
1
1
  ### Context Ports
2
2
 
3
3
  Context ports define the communication available at a context boundary.
4
- They describe what data enters the context, what data leaves it, and which
5
- operation another system can invoke.
4
+ They describe which concrete capability a context exposes, what data enters
5
+ that capability, and what data it returns.
6
6
 
7
- Ports are used to keep communication contracts explicit. An adapter imports a
8
- port, implements that contract, and translates the external interaction into an
9
- application process.
7
+ In practice a port is a specific boundary element of the system with identity:
8
+ a command handler, a query entry point, an event consumer, a published
9
+ endpoint, or another concrete interaction mechanism that exists because the
10
+ running system exposes it.
11
+
12
+ Types and interfaces still matter, but they are secondary. Their role is to
13
+ make the port explicit. The main concern is the port as a real executable
14
+ surface that another actor can call or observe.
10
15
 
11
16
  > **Hint**
12
17
  > The generated `example-ports.ts` file is only a placeholder. Replace it when
@@ -17,149 +22,105 @@ application process.
17
22
  The generated context starts with a single root file for ports.
18
23
 
19
24
  ```ts title="users/example-ports.ts"
20
- export function example(): void {
21
- // ...
25
+ export class ExamplePort {
26
+ public doSomething(): void {
27
+ // ...
28
+ }
22
29
  }
23
30
  ```
24
31
 
25
- This placeholder does not define a real contract yet. Its purpose is to mark
26
- the context root as the place where boundary-facing types and interfaces live.
32
+ This placeholder marks the context root as the place where boundary-facing
33
+ capabilities are declared. Replace it with the first module that defines a real
34
+ port of the context when that capability becomes clear.
35
+
36
+ Context root `.ts` files belong to this same boundary surface. Each one is
37
+ expected to be a module that defines one or more context ports.
27
38
 
28
- #### First Contract
39
+ #### First Port
29
40
 
30
- In the following example we replace the placeholder with a minimal contract for
41
+ In the following example we replace the placeholder with a concrete port for
31
42
  creating a user.
32
43
 
33
44
  ```ts title="users/example-ports.ts"
34
- export type CreateUserRequest = {
35
- user: {
36
- id: string
37
- email: string
38
- active: boolean | null
39
- }
45
+ export interface User {
46
+ id: string
47
+ email: string
48
+ active: boolean | null
40
49
  }
41
50
 
42
- export type CreateUserResponse = {
43
- user: {
44
- id: string
45
- email: string
46
- active: boolean | null
51
+ export class UsersRegistry {
52
+ public async createUser(user: User): Promise<User> {
53
+ // ...
47
54
  }
48
55
  }
49
-
50
- export interface CreateUserPort {
51
- create(request: CreateUserRequest): Promise<CreateUserResponse>
52
- }
53
56
  ```
54
57
 
55
- `CreateUserRequest` defines the incoming payload. `CreateUserResponse` defines
56
- the outgoing payload. `CreateUserPort` exposes the operation available at the
57
- boundary.
58
-
59
- This contract says nothing about HTTP, queues, or databases. It only declares
60
- the shape of the communication.
61
-
62
- #### Adapter Implementation
63
-
64
- Now that the port exists, an adapter can implement it and delegate the work to
65
- an application service.
66
-
67
- ```ts title="users/adapters/create-user.ts"
68
- import type {
69
- CreateUserPort,
70
- CreateUserRequest,
71
- CreateUserResponse,
72
- } from '../example-ports.js'
58
+ `User` expresses the user data that crosses the boundary. `UsersRegistry` is a
59
+ boundary object of the context and `createUser()` is one concrete capability
60
+ that it exposes.
73
61
 
74
- type CreateUserService = {
75
- execute(data: {
76
- id: string
77
- email: string
78
- active: boolean | null
79
- }): Promise<CreateUserResponse>
80
- }
81
-
82
- export class CreateUserAdapter implements CreateUserPort {
83
- public constructor(
84
- private readonly service: CreateUserService,
85
- ) {}
86
-
87
- public async create(
88
- request: CreateUserRequest,
89
- ): Promise<CreateUserResponse> {
90
- return this.service.execute({
91
- id: request.user.id,
92
- email: request.user.email,
93
- active: request.user.active,
94
- })
95
- }
96
- }
97
- ```
62
+ This definition focuses on the interaction the context makes available. The
63
+ port keeps its identity as one boundary capability while making its data and
64
+ action explicit.
98
65
 
99
- The adapter implements `CreateUserPort`, so it must provide `create()`. Inside
100
- that method it translates the root-level request into the input expected by the
101
- application service.
66
+ The port is the executable object that this context exposes. Types help
67
+ describe it and make its boundary explicit.
102
68
 
103
- This is the normal flow of the generated structure:
69
+ This is the normal flow inside the context boundary:
104
70
 
105
- ```text
106
- external system -> adapter -> application -> domain
71
+ ```mermaid
72
+ flowchart LR
73
+ caller["Caller"] --> port[Concrete port]
74
+ port --> application[Application]
75
+ application --> domain[Domain]
107
76
  ```
108
77
 
109
- The port belongs to the boundary. The adapter materializes the boundary. The
110
- application process executes the use case.
78
+ The port belongs to the boundary because it is part of what the context
79
+ exposes. The application process executes the use case behind the exposed
80
+ capability and uses domain capabilities.
111
81
 
112
82
  #### Multiple Port Files
113
83
 
114
- As the context grows, you can keep several contracts at the context root.
84
+ As the context grows, you can keep several port modules at the context root.
115
85
 
116
- ```ts title="users/list-users.ts"
117
- export type ListUsersRequest = {
118
- active: boolean | null
119
- }
120
-
121
- export type ListUsersResponse = {
122
- users: Array<{
123
- id: string
124
- email: string
125
- active: boolean | null
126
- }>
127
- }
128
-
129
- export interface ListUsersPort {
130
- list(request: ListUsersRequest): Promise<ListUsersResponse>
86
+ ```ts title="users/registry.ts"
87
+ export class UsersRegistry {
88
+ public async listUsers(): Promise<User[]> {
89
+ // ...
90
+ }
131
91
  }
132
92
  ```
133
93
 
134
- An adapter can then import the contract from the file that owns it.
94
+ Another caller can then import the port from the file that owns it.
135
95
 
136
- This arrangement is useful when one context exposes several independent forms
137
- of communication. A single file works well for a small context. Separate files
138
- become easier to maintain when responsibilities start to diverge.
96
+ This arrangement is useful when one context exposes several independent
97
+ capabilities. A single file works well for a small context. Separate files
98
+ become easier to maintain when each port has its own identity and
99
+ responsibility.
139
100
 
140
101
  > **Warning**
141
- > A port should define communication, not domain behavior. Avoid moving entity
142
- > rules, repository logic, or infrastructure details into the port file.
102
+ > A port should define an exposed boundary capability.
103
+ > Keep business rules, repository logic, and infrastructure details in their
104
+ > corresponding layers.
143
105
 
144
106
  #### Example Layout
145
107
 
146
108
  The following structure keeps ports at the root while the implementation lives
147
109
  in the generated folders.
148
110
 
149
- ```text
150
- users/
151
- ├── example-ports.ts
152
- ├── list-users.ts
153
- ├── adapters/
154
- ├── application/
155
- └── domain/
111
+ ```mermaid
112
+ flowchart TD
113
+ users["users/"] --> registry["registry.ts"]
114
+ users --> application["application/"]
115
+ users --> domain["domain/"]
156
116
  ```
157
117
 
158
118
  This layout keeps the context boundary visible from the top level. It also
159
- reduces coupling between adapters because they all import the same contracts.
119
+ makes each exposed capability easy to locate because the port modules stay at
120
+ the root of the context.
160
121
 
161
122
  #### Next Step
162
123
 
163
- After defining a port, implement the corresponding adapter and connect it to an
164
- application service. The surrounding structure is described in
165
- `library-structure.md`.
124
+ After defining a port, implement the corresponding executable path and connect
125
+ it to an application service. The surrounding structure is described in
126
+ `library-structure.md`.
@@ -61,14 +61,14 @@ application coordinates data access without forcing a specific driver.
61
61
 
62
62
  #### Context Files
63
63
 
64
- Each generated context starts with a root port file and three directories.
64
+ Each generated context starts with an example port file and three directories.
65
65
 
66
66
  | Path | Responsibility |
67
67
  | --- | --- |
68
- | `users/example-ports.ts` | Placeholder root file for context ports. |
68
+ | `users/example-ports.ts` | Example module for context ports. All context root `.ts` files are expected to define context ports. |
69
69
  | `users/domain/` | Domain capabilities and rules for the context. |
70
70
  | `users/application/` | Processes that use domain capabilities to fulfill system purposes. |
71
- | `users/adapters/` | Integrations that implement ports and connect the context to external systems. |
71
+ | `users/adapters/` | Integrations that wraps third-party libraries or context ports. |
72
72
 
73
73
  The `users/` path is an example context name. Your project can generate one or
74
74
  more contexts with the same internal layout.
@@ -81,8 +81,8 @@ Use the following sequence when deciding where new code belongs.
81
81
  2. Put reusable application contracts in `shared/application`.
82
82
  3. Put context-specific rules in `<context>/domain`.
83
83
  4. Put use cases in `<context>/application`.
84
- 5. Put boundary implementations in `<context>/adapters`.
85
- 6. Put boundary contracts at the context root.
84
+ 5. Put transport and infrastructure integrations in `<context>/adapters`.
85
+ 6. Put concrete boundary ports in context root `.ts` files.
86
86
 
87
87
  This reference explains placement. The architectural rationale is described in
88
- `library-structure.md`.
88
+ `library-structure.md`.
@@ -13,11 +13,12 @@ language, rules, and operations.
13
13
 
14
14
  The root contains the entry points of the generated library.
15
15
 
16
- ```text
17
- index.d.ts
18
- main.ts
19
- shared/
20
- users/
16
+ ```mermaid
17
+ flowchart TD
18
+ root["Library root"] --> index["index.d.ts"]
19
+ root --> main["main.ts"]
20
+ root --> shared["shared/"]
21
+ root --> users["users/"]
21
22
  ```
22
23
 
23
24
  `index.d.ts` defines root-level types. `main.ts` starts as a placeholder for
@@ -29,10 +30,10 @@ or more context directories.
29
30
  The `shared` directory contains concepts that can be reused by multiple
30
31
  contexts.
31
32
 
32
- ```text
33
- shared/
34
- ├── application/
35
- └── domain/
33
+ ```mermaid
34
+ flowchart TD
35
+ shared["shared/"] --> application["application/"]
36
+ shared --> domain["domain/"]
36
37
  ```
37
38
 
38
39
  `shared/domain` contains modeling foundations such as value objects, entities,
@@ -47,11 +48,12 @@ Until then, keep it close to the context that owns the rule.
47
48
  A context groups the vocabulary, rules, and operations of one application
48
49
  capability.
49
50
 
50
- ```text
51
- users/
52
- billing/
53
- inventory/
54
- sales/
51
+ ```mermaid
52
+ flowchart TD
53
+ contexts["Contexts"] --> users["users/"]
54
+ contexts --> billing["billing/"]
55
+ contexts --> inventory["inventory/"]
56
+ contexts --> sales["sales/"]
55
57
  ```
56
58
 
57
59
  Each context can evolve independently while still reusing the abstractions from
@@ -62,17 +64,22 @@ the system.
62
64
 
63
65
  Every generated context starts with the same internal structure.
64
66
 
65
- ```text
66
- users/
67
- ├── example-ports.ts
68
- ├── adapters/
69
- ├── application/
70
- └── domain/
67
+ ```mermaid
68
+ flowchart TD
69
+ users["users/"] --> ports["example-ports.ts"]
70
+ users --> adapters["adapters/"]
71
+ users --> application["application/"]
72
+ users --> domain["domain/"]
71
73
  ```
72
74
 
73
- `example-ports.ts` is the root communication surface of the context.
75
+ `example-ports.ts` is an example module in the root communication surface of
76
+ the context.
74
77
  `domain/` contains capabilities and rules. `application/` contains processes.
75
- `adapters/` contains boundary implementations.
78
+ `adapters/` contains integrations that wrap third-party libraries or context
79
+ ports.
80
+
81
+ Context root `.ts` files belong to the boundary surface of the context. Each
82
+ one is expected to be a module that defines one or more context ports.
76
83
 
77
84
  #### Domain
78
85
 
@@ -103,34 +110,40 @@ business rules themselves.
103
110
  The adapters layer contains the integrations that connect a context to other
104
111
  systems.
105
112
 
106
- An adapter can expose an HTTP handler, consume a message, call a remote API,
107
- implement a data driver, or connect to an event bus. Its role is to translate
108
- external input or output into the contracts expected by the application layer.
113
+ An adapter wraps a third-party library or a context port so that the context
114
+ can interact with a concrete transport or infrastructure path. An adapter can
115
+ expose an HTTP handler, consume a message, call a remote API, implement a data
116
+ driver, or connect to an event bus.
109
117
 
110
118
  #### Ports
111
119
 
112
120
  Ports define the communication available at the context boundary.
113
121
 
114
122
  They live at the context root because they describe how the context is used
115
- from the outside. An adapter imports a port, implements it, and delegates the
116
- work to an application process.
123
+ from the outside. A port is the concrete object the context exposes. Adapters
124
+ or other callers can use that port object and route work into an application
125
+ process.
117
126
 
118
- The generated template starts with `example-ports.ts`, but a larger context may
119
- split ports across several files. The detailed guidance for that layout lives in
120
- `context-ports.md`.
127
+ The generated template starts with `example-ports.ts`. As the context grows,
128
+ additional context root `.ts` modules can define more ports. The detailed
129
+ guidance for that layout lives in `context-ports.md`.
121
130
 
122
131
  #### Dependency Direction
123
132
 
124
133
  The normal dependency direction is the following:
125
134
 
126
- ```text
127
- adapter -> port
128
- adapter -> application
129
- application -> domain
135
+ ```mermaid
136
+ flowchart LR
137
+ adapter[Adapter] --> thirdParty["Third-party library"]
138
+ adapter --> port[Port]
139
+ port --> application[Application]
140
+ application --> domain
130
141
  ```
131
142
 
132
143
  This direction keeps the core model isolated from transport and infrastructure
133
- details. The deeper a layer is, the less it should know about the outside.
144
+ details. Adapters integrate with external libraries and context ports. Ports
145
+ connect the context boundary to application processes. The deeper a layer is,
146
+ the less it should know about the outside.
134
147
 
135
148
  > **Warning**
136
149
  > Avoid importing adapter-specific concerns into the domain layer. Once a domain
@@ -141,23 +154,23 @@ details. The deeper a layer is, the less it should know about the outside.
141
154
 
142
155
  The following diagram shows the runtime flow of a typical operation.
143
156
 
144
- ```text
145
- External system
146
- |
147
- v
148
- Port + adapter
149
- |
150
- v
151
- Application service
152
- |
153
- v
154
- Domain capability
157
+ ```mermaid
158
+ flowchart TD
159
+ main["main.ts"] --> system["Own system"]
160
+ system --> application["Application services"]
161
+ application --> domain["Domain capabilities"]
162
+ system --> adapter[Adapters]
163
+ adapter --> port[Port]
164
+ adapter --> thirdParty["Third-party libraries"]
165
+ thirdParty --> external["External systems"]
155
166
  ```
156
167
 
157
- The adapter receives the input, the application service coordinates the use
158
- case, and the domain provides the rules and behavior required by that use case.
168
+ `main.ts` is the runtime entry point into the own system. Inside that system,
169
+ application services use domain capabilities, while adapters can depend on
170
+ ports and third-party libraries. The port branch stops at the boundary because
171
+ what exists beyond that port depends on the system that implements it.
159
172
 
160
173
  #### Next Step
161
174
 
162
175
  Use this structure as the default layout for new code. When you need to inspect
163
- the purpose of a generated file, consult `generated-file-reference.md`.
176
+ the purpose of a generated file, consult `generated-file-reference.md`.
@@ -176,10 +176,15 @@ and the representation used by the context.
176
176
 
177
177
  The normal flow of the data abstractions is the following:
178
178
 
179
- ```text
180
- service -> repository -> driver -> data manager -> raw records
181
- service <- repository <- transformed records
179
+ ```mermaid
180
+ flowchart LR
181
+ service[Service] --> repository[Repository]
182
+ repository --> driver[Driver]
183
+ driver --> manager["Data manager"]
184
+ manager --> raw["Raw records"]
185
+ repository --> transformed["Transformed records"]
186
+ transformed --> service
182
187
  ```
183
188
 
184
189
  This separation keeps the application service focused on orchestration while
185
- the repository focuses on transformation.
190
+ the repository focuses on transformation.
@@ -135,10 +135,12 @@ registered or executed.
135
135
 
136
136
  The normal flow is the following:
137
137
 
138
- ```text
139
- service -> dispatch(event)
140
- dispatcher -> matching handlers
141
- handlers -> side effects
138
+ ```mermaid
139
+ flowchart LR
140
+ service[Service] --> dispatch["dispatch(event)"]
141
+ dispatch --> dispatcher[Dispatcher]
142
+ dispatcher --> handlers["Matching handlers"]
143
+ handlers --> effects["Side effects"]
142
144
  ```
143
145
 
144
- This keeps the main process separate from secondary reactions.
146
+ This keeps the main process separate from secondary reactions.
@@ -123,9 +123,12 @@ cross-cutting behavior belongs in the generated HTTP abstraction.
123
123
 
124
124
  #### Example Flow
125
125
 
126
- ```text
127
- request -> middleware -> handler -> response body
126
+ ```mermaid
127
+ flowchart LR
128
+ request[Request] --> middleware[Middleware]
129
+ middleware --> handler[Handler]
130
+ handler --> response["Response body"]
128
131
  ```
129
132
 
130
133
  This flow keeps the transport boundary explicit while leaving framework choices
131
- to the adapter layer.
134
+ to the adapter layer.
@@ -120,8 +120,11 @@ an external platform. It only depends on the application-level contract.
120
120
 
121
121
  #### Example Flow
122
122
 
123
- ```text
124
- service -> Logger contract -> adapter -> logging backend
123
+ ```mermaid
124
+ flowchart LR
125
+ service[Service] --> contract["Logger contract"]
126
+ contract --> adapter[Adapter]
127
+ adapter --> backend["Logging backend"]
125
128
  ```
126
129
 
127
- This flow keeps observability concerns outside the core process.
130
+ This flow keeps observability concerns outside the core process.
@@ -112,9 +112,13 @@ result, depending on the requirements of the use case.
112
112
 
113
113
  #### Example Flow
114
114
 
115
- ```text
116
- input -> service -> domain capabilities -> collaborators -> result
115
+ ```mermaid
116
+ flowchart LR
117
+ input[Input] --> service[Service]
118
+ service --> domain["Domain capabilities"]
119
+ domain --> collaborators[Collaborators]
120
+ collaborators --> result[Result]
117
121
  ```
118
122
 
119
123
  This flow keeps orchestration in the application layer and domain meaning in
120
- the domain layer.
124
+ the domain layer.
@@ -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.23",
3
+ "version": "1.0.25",
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
  },
package/source/main.ts CHANGED
@@ -55,7 +55,13 @@ function executeCreateReactContext(templatesDir: string, contextDir: string) {
55
55
  }
56
56
  }
57
57
 
58
- function executeCreateTests(sourceDir: string, destinationDir: string, fileContents: string, ignoredSourceDir?: string) {
58
+ function executeCreateTests(
59
+ sourceDir: string,
60
+ destinationDir: string,
61
+ fileContents: string,
62
+ ignoredSourceDir?: string,
63
+ rootSourceDir: string = sourceDir
64
+ ) {
59
65
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
60
66
 
61
67
  if (fs.existsSync(destinationDir) === false) {
@@ -67,7 +73,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
67
73
  const destinationPath = path.join(destinationDir, entry.name)
68
74
 
69
75
  if (entry.isDirectory()) {
70
- if (entry.name === 'shared') {
76
+ if (entry.name === 'shared' && sourceDir === rootSourceDir) {
71
77
  continue
72
78
  }
73
79
 
@@ -79,7 +85,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
79
85
  continue
80
86
  }
81
87
 
82
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir)
88
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir)
83
89
  continue
84
90
  }
85
91