@ankhorage/devtools 1.14.2 → 1.14.4
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/dist/tools/skills/assets/ankhorage-project-structure/SKILL.md +53 -70
- package/dist/tools/skills/assets/hexagonal-architecture/SKILL.md +256 -52
- package/dist/tools/workflows/files/renovate.json5 +7 -0
- package/dist/tools/workflows/index.d.ts +1 -1
- package/dist/tools/workflows/index.js +21 -1
- package/package.json +1 -1
|
@@ -1,41 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ankhorage-project-structure
|
|
3
3
|
description: >
|
|
4
|
-
Define, review, or implement the standard source structure of Ankhorage repositories. Use for
|
|
5
|
-
feature ownership, CLI layout, hexagonal boundaries, source-module naming, type ownership,
|
|
6
|
-
utilities, or package entrypoints.
|
|
4
|
+
Define, review, or implement the standard source structure of Ankhorage repositories. Use for feature ownership, CLI layout, hexagonal boundaries, source-module naming, type ownership, utilities, or package entrypoints.
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
# Ankhorage Project Structure
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
`ankhorage/deploy`, `ankhorage/infra`, `ankhorage/repository`, and `ankhorage/navigator`.
|
|
9
|
+
## Applicability
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Before structural work, read the repository `AGENTS.md`, inspect its source tree and public
|
|
17
|
-
exports, then load both required repository skills:
|
|
11
|
+
This skill applies to every Ankhorage repository except `ankhorage/contracts`.
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
If the current repository is `ankhorage/contracts`, stop applying this skill. Contracts owns the
|
|
14
|
+
portable cross-repository contract taxonomy and does not inherit this skill's required `src/cli/`,
|
|
15
|
+
`src/features/`, hexagonal feature layout, type/constant/utility ownership, package facade, or
|
|
16
|
+
repository migration rules.
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
Cannot continue: the required repository skill `ankhorage-coding-rules` is missing or unreadable at `.agents/skills/ankhorage-coding-rules/SKILL.md`. Synchronize the repository skills from `@ankhorage/devtools` and retry.
|
|
26
|
-
```
|
|
18
|
+
## Required skills
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
Before structural work, read the repository `AGENTS.md`, inspect its source tree and public
|
|
21
|
+
exports, then load both required repository skills from the repository root. Do not resolve required
|
|
22
|
+
skills relative to this skill's own installation location:
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
24
|
+
1. `<repo-root>/.agents/skills/ankhorage-coding-rules/SKILL.md`
|
|
25
|
+
2. `<repo-root>/.agents/skills/hexagonal-architecture/SKILL.md`
|
|
33
26
|
|
|
34
|
-
##
|
|
27
|
+
## Required source layout
|
|
35
28
|
|
|
36
|
-
`examples
|
|
37
|
-
Use it for complete, intentional, user-facing examples that people can inspect, copy, install, and
|
|
38
|
-
run independently of a monorepo or internal fixture layout.
|
|
29
|
+
- `examples/`: Repository-root folder in this standalone repository;
|
|
30
|
+
Use it for complete, intentional, user-facing examples that people can inspect, copy, install, and run independently of a monorepo or internal fixture layout.
|
|
39
31
|
|
|
40
32
|
Each example lives in a named subdirectory, such as `examples/basic-usage/*.ts`. Do not put example
|
|
41
33
|
source files directly under `examples/`.
|
|
@@ -43,17 +35,29 @@ source files directly under `examples/`.
|
|
|
43
35
|
Test-only fixtures remain owned by the applicable test structure. Do not relabel fixtures as public
|
|
44
36
|
examples merely to bypass repository structure rules.
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
- `src/cli/` must exist or have a concrete issue tracking the missing CLI commands;
|
|
39
|
+
CLI modules are thin inbound adapters: they parse input, invoke a feature use case, and render
|
|
40
|
+
output.
|
|
41
|
+
|
|
42
|
+
The filesystem below `src/cli/commands/` mirrors the public command path after the package prefix:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
ankh <package> <segment> ... <command>
|
|
46
|
+
-> src/cli/commands/<segment>/.../<command>.ts
|
|
47
|
+
```
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
The package prefix is represented by the provider and is not repeated under `commands/`. Flags and
|
|
50
|
+
positional arguments do not affect this directory tree. Each command file follows the one-export
|
|
51
|
+
rule: `commands/projects/list.ts` exports `list` and owns only the command-specific input/output
|
|
52
|
+
mapping.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
- `src/features/`: Lists the repository's actual product capabilities;
|
|
55
|
+
Technical categories are not features. Each feature owns its own hexagonal structure as needed,
|
|
56
|
+
following the required Hexagonal Architecture skill. Do not create empty layers.
|
|
55
57
|
|
|
56
58
|
```text
|
|
59
|
+
examples/
|
|
60
|
+
<example>/
|
|
57
61
|
src/
|
|
58
62
|
cli/
|
|
59
63
|
createCliProvider.ts
|
|
@@ -73,42 +77,33 @@ src/
|
|
|
73
77
|
inbound/
|
|
74
78
|
outbound/
|
|
75
79
|
composition/
|
|
80
|
+
constants/
|
|
81
|
+
<topic>.ts
|
|
76
82
|
utils/
|
|
77
83
|
types/
|
|
78
84
|
<topic>.ts
|
|
79
|
-
constants
|
|
85
|
+
constants/
|
|
86
|
+
<topic>.ts
|
|
80
87
|
utils/
|
|
81
88
|
```
|
|
82
89
|
|
|
83
90
|
Keep only deliberate package facades directly under `src/`. Public package subpaths must name their
|
|
84
91
|
explicit module in `package.json`; generic `index.ts` barrels are not public API exceptions.
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```text
|
|
89
|
-
ankh <package> <segment> ... <command>
|
|
90
|
-
-> src/cli/commands/<segment>/.../<command>.ts
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
The package prefix is represented by the provider and is not repeated under `commands/`. Flags and
|
|
94
|
-
positional arguments do not affect this directory tree. Each command file follows the one-export
|
|
95
|
-
rule: `commands/projects/list.ts` exports `list` and owns only the command-specific input/output
|
|
96
|
-
mapping.
|
|
97
|
-
|
|
98
|
-
## Feature taxonomy
|
|
93
|
+
## General Taxonomy
|
|
99
94
|
|
|
100
95
|
Siblings always represent the same kind of entity. A folder cannot be an unrelated catch-all beside
|
|
101
|
-
peer entities. For example, this is invalid because `
|
|
96
|
+
peer entities. For example, this is invalid because `other/` is not a color:
|
|
102
97
|
|
|
103
98
|
```text
|
|
104
99
|
colors/
|
|
105
100
|
red/
|
|
106
101
|
green/
|
|
107
102
|
blue/
|
|
108
|
-
|
|
103
|
+
other/
|
|
109
104
|
```
|
|
110
105
|
|
|
111
|
-
Resolve the ownership of `
|
|
106
|
+
Resolve the ownership of `other` and move it to the appropriate taxonomy. Use domain names for
|
|
112
107
|
features, not framework, transport, database, or generic technical names.
|
|
113
108
|
|
|
114
109
|
## Implementation modules
|
|
@@ -121,8 +116,7 @@ convenience barrels and must not expose private implementation details.
|
|
|
121
116
|
|
|
122
117
|
- `myFunction.ts` exports `myFunction`.
|
|
123
118
|
- `myFunctionAsync.ts` exports `myFunctionAsync`.
|
|
124
|
-
- A public operation that is asynchronous or returns a `Promise` uses the `Async` suffix in both its
|
|
125
|
-
filename and exported name.
|
|
119
|
+
- A public operation that is asynchronous or returns a `Promise` uses the `Async` suffix in both its filename and exported name.
|
|
126
120
|
|
|
127
121
|
Keep private helpers below that exported declaration when they are used only by that module.
|
|
128
122
|
Decide the owner of a reused function using the utility rules below, before creating another file.
|
|
@@ -132,17 +126,9 @@ Decide the owner of a reused function using the utility rules below, before crea
|
|
|
132
126
|
Choose type ownership by its production consumers, not by the number of textual references or
|
|
133
127
|
whether a barrel happens to re-export it:
|
|
134
128
|
|
|
135
|
-
1. **Used by one implementation module:** keep the type directly below the function that owns it,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
2. **Reused within the repository:** put related types together in `src/types/<topic>.ts` and use
|
|
139
|
-
type-only imports. Name the file for a cohesive topic, not for each individual type. Such a file
|
|
140
|
-
may export multiple related types/interfaces and contains no runtime implementation. Do not mix
|
|
141
|
-
type-only files among feature functions or `utils/`, and do not create one global catch-all file.
|
|
142
|
-
3. **Shared across repositories:** the canonical declaration belongs in `@ankhorage/contracts` at
|
|
143
|
-
the owning topic's public subpath. Consumers import that contract through a declared dependency,
|
|
144
|
-
not another repository's source or a duplicated local declaration. Keep framework-specific
|
|
145
|
-
adapters separate from the portable shared contract.
|
|
129
|
+
1. **Used by one implementation module:** keep the type directly below the function that owns it, without `export`. Its private helpers can use the same local type. A test does not justify exporting an implementation-private type; test through the function boundary.
|
|
130
|
+
2. **Reused within the repository:** put related types together in `src/types/<topic>.ts` and use type-only imports. Name the file for a cohesive topic, not for each individual type. Such a file may export multiple related types/interfaces and contains no runtime implementation. Do not mix type-only files among feature functions or `utils/`, and do not create one global catch-all file.
|
|
131
|
+
3. **Shared across repositories:** the canonical declaration belongs in `@ankhorage/contracts` at the owning topic's public subpath. Consumers import that contract through a declared dependency, not another repository's source or a duplicated local declaration. Keep framework-specific adapters separate from the portable shared contract.
|
|
146
132
|
|
|
147
133
|
Inspect published API declarations and real consumer imports before privatizing or relocating a
|
|
148
134
|
type. A public boundary type is not private just because only one implementation uses it locally.
|
|
@@ -161,11 +147,10 @@ Constants are static declarations, not utility implementations. Do not create on
|
|
|
161
147
|
constant per constant-named file under `utils/`.
|
|
162
148
|
|
|
163
149
|
1. **Used by one implementation module:** keep the constant private in the module that owns it.
|
|
164
|
-
2. **Reused only inside a feature:** group related constants in that feature's `constants
|
|
165
|
-
3. **Shared across features in one package:** group related package metadata, static policy values,
|
|
166
|
-
and other constants in `src/constants.ts`.
|
|
150
|
+
2. **Reused only inside a feature:** group related constants in that feature's `constants/<topic>.ts`.
|
|
151
|
+
3. **Shared across features in one package:** group related package metadata, static policy values, and other constants in `src/constants/<topic>.ts`.
|
|
167
152
|
|
|
168
|
-
A `constants
|
|
153
|
+
A `constants/<topic>.ts` module may export multiple related constants. Keep it cohesive by ownership and
|
|
169
154
|
purpose; it is not a package-wide catch-all. Split constants when they have different owners, not
|
|
170
155
|
merely to create one file per export.
|
|
171
156
|
|
|
@@ -173,7 +158,7 @@ For example, Navigator's
|
|
|
173
158
|
[`src/utils/NAVIGATOR_PACKAGE_METADATA.ts`](https://github.com/ankhorage/navigator/blob/main/src/utils/NAVIGATOR_PACKAGE_METADATA.ts)
|
|
174
159
|
and
|
|
175
160
|
[`src/utils/NAVIGATOR_ROUTER_POLICY.ts`](https://github.com/ankhorage/navigator/blob/main/src/utils/NAVIGATOR_ROUTER_POLICY.ts)
|
|
176
|
-
belong together in `ankhorage/navigator/src/constants.ts`.
|
|
161
|
+
belong together in `ankhorage/navigator/src/constants/navigator.ts`.
|
|
177
162
|
|
|
178
163
|
## Utilities
|
|
179
164
|
|
|
@@ -187,13 +172,11 @@ belong together in `ankhorage/navigator/src/constants.ts`.
|
|
|
187
172
|
merely because several navigator features use it.
|
|
188
173
|
- Generally reusable without the owning product, manifest, or framework policy: inspect the
|
|
189
174
|
published `@ankhorage/utility` API first, reuse it where semantics match, and put missing general
|
|
190
|
-
helpers in that package's owning topic. Examples include generic string escaping or source-literal
|
|
191
|
-
serialization. Do not copy a utility locally, create a forwarding wrapper, or change semantics
|
|
192
|
-
just to reuse a similarly named function.
|
|
175
|
+
helpers in that package's owning topic. Examples include generic string escaping or source-literal serialization. Do not copy a utility locally, create a forwarding wrapper, or change semantics just to reuse a similarly named function.
|
|
193
176
|
|
|
194
177
|
Separate the decisions for functions and types: reusable functions belong to Utility when general;
|
|
195
178
|
repo-local type groups belong to `src/types/`; repo-crossing types belong to Contracts. Respect
|
|
196
179
|
release boundaries and obtain approval for additional package changes when they exceed the task.
|
|
197
180
|
|
|
198
181
|
This skill defines the target architecture. Schedule repository migrations separately and in this
|
|
199
|
-
order: Studio, Deploy, Infra, Repository, Navigator.
|
|
182
|
+
order: Studio, Deploy, Infra, Repository, Navigator, Surface, ZORA.
|
|
@@ -1,72 +1,276 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hexagonal-architecture
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
transport, frameworks, and infrastructure.
|
|
6
|
-
origin: ECC, adapted for Ankhorage
|
|
7
|
-
metadata:
|
|
8
|
-
github-repo: https://github.com/affaan-m/ECC
|
|
9
|
-
github-ref: refs/tags/v2.2.0
|
|
10
|
-
github-path: docs/ja-JP/skills/hexagonal-architecture
|
|
3
|
+
description: ヘキサゴナルアーキテクチャ(ポート・アダプタパターン)、境界の分離、および外部依存関係の管理。
|
|
4
|
+
origin: ECC
|
|
11
5
|
---
|
|
12
6
|
|
|
13
7
|
# Hexagonal Architecture
|
|
14
8
|
|
|
15
|
-
Hexagonal architecture
|
|
16
|
-
and infrastructure details. The application layer depends on ports; inbound and outbound adapters
|
|
17
|
-
implement the edge-specific work.
|
|
9
|
+
Hexagonal architecture (Ports and Adapters) keeps business logic independent from frameworks, transport, and persistence details. The core app depends on abstract ports, and adapters implement those ports at the edges.
|
|
18
10
|
|
|
19
|
-
##
|
|
11
|
+
## When to Use
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
- Building new features where long-term maintainability and testability matter.
|
|
14
|
+
- Refactoring layered or framework-heavy code where domain logic is mixed with I/O concerns.
|
|
15
|
+
- Supporting multiple interfaces for the same use case (HTTP, CLI, queue workers, cron jobs).
|
|
16
|
+
- Replacing infrastructure (database, external APIs, message bus) without rewriting business rules.
|
|
17
|
+
|
|
18
|
+
Use this skill when the request involves boundaries, domain-centric design, refactoring tightly coupled services, or decoupling application logic from specific libraries.
|
|
19
|
+
|
|
20
|
+
## Core Concepts
|
|
21
|
+
|
|
22
|
+
- **Domain model**: Business rules and entities/value objects. No framework imports.
|
|
23
|
+
- **Use cases (application layer)**: Orchestrate domain behavior and workflow steps.
|
|
24
|
+
- **Inbound ports**: Contracts describing what the application can do (commands/queries/use-case interfaces).
|
|
25
|
+
- **Outbound ports**: Contracts for dependencies the application needs (repositories, gateways, event publishers, clock, UUID, etc.).
|
|
26
|
+
- **Adapters**: Infrastructure and delivery implementations of ports (HTTP controllers, DB repositories, queue consumers, SDK wrappers).
|
|
27
|
+
- **Composition root**: Single wiring location where concrete adapters are bound to use cases.
|
|
28
|
+
|
|
29
|
+
Outbound port interfaces usually live in the application layer (or in domain only when the abstraction is truly domain-level), while infrastructure adapters implement them.
|
|
30
|
+
|
|
31
|
+
Dependency direction is always inward:
|
|
32
|
+
|
|
33
|
+
- Adapters -> application/domain
|
|
34
|
+
- Application -> port interfaces (inbound/outbound contracts)
|
|
35
|
+
- Domain -> domain-only abstractions (no framework or infrastructure dependencies)
|
|
36
|
+
- Domain -> nothing external
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
### Step 1: Model a use case boundary
|
|
41
|
+
|
|
42
|
+
Define a single use case with a clear input and output DTO. Keep transport details (Express `req`, GraphQL `context`, job payload wrappers) outside this boundary.
|
|
43
|
+
|
|
44
|
+
### Step 2: Define outbound ports first
|
|
45
|
+
|
|
46
|
+
Identify every side effect as a port:
|
|
47
|
+
|
|
48
|
+
- persistence (`UserRepositoryPort`)
|
|
49
|
+
- external calls (`BillingGatewayPort`)
|
|
50
|
+
- cross-cutting (`LoggerPort`, `ClockPort`)
|
|
51
|
+
|
|
52
|
+
Ports should model capabilities, not technologies.
|
|
53
|
+
|
|
54
|
+
### Step 3: Implement the use case with pure orchestration
|
|
55
|
+
|
|
56
|
+
Use case class/function receives ports via constructor/arguments. It validates application-level invariants, coordinates domain rules, and returns plain data structures.
|
|
57
|
+
|
|
58
|
+
### Step 4: Build adapters at the edge
|
|
59
|
+
|
|
60
|
+
- Inbound adapter converts protocol input to use-case input.
|
|
61
|
+
- Outbound adapter maps app contracts to concrete APIs/ORM/query builders.
|
|
62
|
+
- Mapping stays in adapters, not inside use cases.
|
|
63
|
+
|
|
64
|
+
### Step 5: Wire everything in a composition root
|
|
65
|
+
|
|
66
|
+
Instantiate adapters, then inject them into use cases. Keep this wiring centralized to avoid hidden service-locator behavior.
|
|
67
|
+
|
|
68
|
+
### Step 6: Test per boundary
|
|
69
|
+
|
|
70
|
+
- Unit test use cases with fake ports.
|
|
71
|
+
- Integration test adapters with real infra dependencies.
|
|
72
|
+
- E2E test user-facing flows through inbound adapters.
|
|
73
|
+
|
|
74
|
+
## Architecture Diagram
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
flowchart LR
|
|
78
|
+
Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
|
|
79
|
+
InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
|
|
80
|
+
UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
|
|
81
|
+
OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
|
|
82
|
+
OutboundAdapter --> ExternalSystem["DB/API/Queue"]
|
|
83
|
+
UseCase --> DomainModel["DomainModel"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Suggested Module Layout
|
|
87
|
+
|
|
88
|
+
Use feature-first organization with explicit boundaries:
|
|
23
89
|
|
|
24
90
|
```text
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
91
|
+
src/
|
|
92
|
+
features/
|
|
93
|
+
orders/
|
|
94
|
+
domain/
|
|
95
|
+
Order.ts
|
|
96
|
+
OrderPolicy.ts
|
|
97
|
+
application/
|
|
98
|
+
ports/
|
|
99
|
+
inbound/
|
|
100
|
+
CreateOrder.ts
|
|
101
|
+
outbound/
|
|
102
|
+
OrderRepositoryPort.ts
|
|
103
|
+
PaymentGatewayPort.ts
|
|
104
|
+
use-cases/
|
|
105
|
+
CreateOrderUseCase.ts
|
|
106
|
+
adapters/
|
|
107
|
+
inbound/
|
|
108
|
+
http/
|
|
109
|
+
createOrderRoute.ts
|
|
110
|
+
outbound/
|
|
111
|
+
postgres/
|
|
112
|
+
PostgresOrderRepository.ts
|
|
113
|
+
stripe/
|
|
114
|
+
StripePaymentGateway.ts
|
|
115
|
+
composition/
|
|
116
|
+
ordersContainer.ts
|
|
37
117
|
```
|
|
38
118
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
119
|
+
## TypeScript Example
|
|
120
|
+
|
|
121
|
+
### Port definitions
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export interface OrderRepositoryPort {
|
|
125
|
+
save(order: Order): Promise<void>;
|
|
126
|
+
findById(orderId: string): Promise<Order | null>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PaymentGatewayPort {
|
|
130
|
+
authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Use case
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
type CreateOrderInput = {
|
|
138
|
+
orderId: string;
|
|
139
|
+
amountCents: number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type CreateOrderOutput = {
|
|
143
|
+
orderId: string;
|
|
144
|
+
authorizationId: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export class CreateOrderUseCase {
|
|
148
|
+
constructor(
|
|
149
|
+
private readonly orderRepository: OrderRepositoryPort,
|
|
150
|
+
private readonly paymentGateway: PaymentGatewayPort,
|
|
151
|
+
) {}
|
|
152
|
+
|
|
153
|
+
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
|
|
154
|
+
const order = Order.create({ id: input.orderId, amountCents: input.amountCents });
|
|
155
|
+
|
|
156
|
+
const auth = await this.paymentGateway.authorize({
|
|
157
|
+
orderId: order.id,
|
|
158
|
+
amountCents: order.amountCents,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// markAuthorized returns a new Order instance; it does not mutate in place.
|
|
162
|
+
const authorizedOrder = order.markAuthorized(auth.authorizationId);
|
|
163
|
+
await this.orderRepository.save(authorizedOrder);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
orderId: order.id,
|
|
167
|
+
authorizationId: auth.authorizationId,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Outbound adapter
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
export class PostgresOrderRepository implements OrderRepositoryPort {
|
|
177
|
+
constructor(private readonly db: SqlClient) {}
|
|
178
|
+
|
|
179
|
+
async save(order: Order): Promise<void> {
|
|
180
|
+
await this.db.query(
|
|
181
|
+
'insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)',
|
|
182
|
+
[order.id, order.amountCents, order.status, order.authorizationId],
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async findById(orderId: string): Promise<Order | null> {
|
|
187
|
+
const row = await this.db.oneOrNone('select * from orders where id = $1', [orderId]);
|
|
188
|
+
return row ? Order.rehydrate(row) : null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Composition root
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
|
|
197
|
+
const orderRepository = new PostgresOrderRepository(deps.db);
|
|
198
|
+
const paymentGateway = new StripePaymentGateway(deps.stripe);
|
|
199
|
+
|
|
200
|
+
return new CreateOrderUseCase(orderRepository, paymentGateway);
|
|
201
|
+
};
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Multi-Language Mapping
|
|
205
|
+
|
|
206
|
+
Use the same boundary rules across ecosystems; only syntax and wiring style change.
|
|
207
|
+
|
|
208
|
+
- **TypeScript/JavaScript**
|
|
209
|
+
- Ports: `application/ports/*` as interfaces/types.
|
|
210
|
+
- Use cases: classes/functions with constructor/argument injection.
|
|
211
|
+
- Adapters: `adapters/inbound/*`, `adapters/outbound/*`.
|
|
212
|
+
- Composition: explicit factory/container module (no hidden globals).
|
|
213
|
+
- **Java**
|
|
214
|
+
- Packages: `domain`, `application.port.in`, `application.port.out`, `application.usecase`, `adapter.in`, `adapter.out`.
|
|
215
|
+
- Ports: interfaces in `application.port.*`.
|
|
216
|
+
- Use cases: plain classes (Spring `@Service` is optional, not required).
|
|
217
|
+
- Composition: Spring config or manual wiring class; keep wiring out of domain/use-case classes.
|
|
218
|
+
- **Kotlin**
|
|
219
|
+
- Modules/packages mirror the Java split (`domain`, `application.port`, `application.usecase`, `adapter`).
|
|
220
|
+
- Ports: Kotlin interfaces.
|
|
221
|
+
- Use cases: classes with constructor injection (Koin/Dagger/Spring/manual).
|
|
222
|
+
- Composition: module definitions or dedicated composition functions; avoid service locator patterns.
|
|
223
|
+
- **Go**
|
|
224
|
+
- Packages: `internal/<feature>/domain`, `application`, `ports`, `adapters/inbound`, `adapters/outbound`.
|
|
225
|
+
- Ports: small interfaces owned by the consuming application package.
|
|
226
|
+
- Use cases: structs with interface fields plus explicit `New...` constructors.
|
|
227
|
+
- Composition: wire in `cmd/<app>/main.go` (or dedicated wiring package), keep constructors explicit.
|
|
228
|
+
|
|
229
|
+
## Anti-Patterns to Avoid
|
|
230
|
+
|
|
231
|
+
- Domain entities importing ORM models, web framework types, or SDK clients.
|
|
232
|
+
- Use cases reading directly from `req`, `res`, or queue metadata.
|
|
233
|
+
- Returning database rows directly from use cases without domain/application mapping.
|
|
234
|
+
- Letting adapters call each other directly instead of flowing through use-case ports.
|
|
235
|
+
- Spreading dependency wiring across many files with hidden global singletons.
|
|
47
236
|
|
|
48
|
-
|
|
49
|
-
depends on domain and ports; domain has no external dependencies. Adapters do not call each other
|
|
50
|
-
to bypass a use case.
|
|
237
|
+
## Migration Playbook
|
|
51
238
|
|
|
52
|
-
|
|
239
|
+
1. Pick one vertical slice (single endpoint/job) with frequent change pain.
|
|
240
|
+
2. Extract a use-case boundary with explicit input/output types.
|
|
241
|
+
3. Introduce outbound ports around existing infrastructure calls.
|
|
242
|
+
4. Move orchestration logic from controllers/services into the use case.
|
|
243
|
+
5. Keep old adapters, but make them delegate to the new use case.
|
|
244
|
+
6. Add tests around the new boundary (unit + adapter integration).
|
|
245
|
+
7. Repeat slice-by-slice; avoid full rewrites.
|
|
53
246
|
|
|
54
|
-
|
|
55
|
-
each capability the application needs from persistence, an external system, or cross-cutting runtime
|
|
56
|
-
services. Model ports as capabilities rather than technologies.
|
|
247
|
+
### Refactoring Existing Systems
|
|
57
248
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
249
|
+
- **Strangler approach**: keep current endpoints, route one use case at a time through new ports/adapters.
|
|
250
|
+
- **No big-bang rewrites**: migrate per feature slice and preserve behavior with characterization tests.
|
|
251
|
+
- **Facade first**: wrap legacy services behind outbound ports before replacing internals.
|
|
252
|
+
- **Composition freeze**: centralize wiring early so new dependencies do not leak into domain/use-case layers.
|
|
253
|
+
- **Slice selection rule**: prioritize high-churn, low-blast-radius flows first.
|
|
254
|
+
- **Rollback path**: keep a reversible toggle or route switch per migrated slice until production behavior is verified.
|
|
61
255
|
|
|
62
|
-
## Testing
|
|
256
|
+
## Testing Guidance (Same Hexagonal Boundaries)
|
|
63
257
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
258
|
+
- **Domain tests**: test entities/value objects as pure business rules (no mocks, no framework setup).
|
|
259
|
+
- **Use-case unit tests**: test orchestration with fakes/stubs for outbound ports; assert business outcomes and port interactions.
|
|
260
|
+
- **Outbound adapter contract tests**: define shared contract suites at port level and run them against each adapter implementation.
|
|
261
|
+
- **Inbound adapter tests**: verify protocol mapping (HTTP/CLI/queue payload to use-case input and output/error mapping back to protocol).
|
|
262
|
+
- **Adapter integration tests**: run against real infrastructure (DB/API/queue) for serialization, schema/query behavior, retries, and timeouts.
|
|
263
|
+
- **End-to-end tests**: cover critical user journeys through inbound adapter -> use case -> outbound adapter.
|
|
264
|
+
- **Refactor safety**: add characterization tests before extraction; keep them until new boundary behavior is stable and equivalent.
|
|
67
265
|
|
|
68
|
-
##
|
|
266
|
+
## Best Practices Checklist
|
|
69
267
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
268
|
+
- Domain and use-case layers import only internal types and ports.
|
|
269
|
+
- Every external dependency is represented by an outbound port.
|
|
270
|
+
- Validation occurs at boundaries (inbound adapter + use-case invariants).
|
|
271
|
+
- Use immutable transformations (return new values/entities instead of mutating shared state).
|
|
272
|
+
- Errors are translated across boundaries (infra errors -> application/domain errors).
|
|
273
|
+
- Composition root is explicit and easy to audit.
|
|
274
|
+
- Use cases are testable with simple in-memory fakes for ports.
|
|
275
|
+
- Refactoring starts from one vertical slice with behavior-preserving tests.
|
|
276
|
+
- Language/framework specifics stay in adapters, never in domain rules.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ManagedFileDefinition } from '../shared/managedFiles.js';
|
|
2
|
-
export declare const workflowManagedFiles: readonly [
|
|
2
|
+
export declare const workflowManagedFiles: readonly ManagedFileDefinition[];
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { bunRuntimePolicy, nodeRuntimePolicy } from '../../policy/bunRuntimePolicy.js';
|
|
2
4
|
import { renderRenovateWorkflowAsync } from './renderRenovateWorkflowAsync.js';
|
|
3
5
|
import { renderWorkflowAsync } from './renderWorkflowAsync.js';
|
|
4
6
|
export const workflowManagedFiles = [
|
|
5
7
|
createWorkflowDefinition('.github/workflows/ci.yml', './files/ci.yml'),
|
|
6
|
-
|
|
8
|
+
{
|
|
9
|
+
...createWorkflowDefinition('.github/workflows/release.yml', './files/release.yml'),
|
|
10
|
+
isApplicable: async (targetDirectory) => {
|
|
11
|
+
try {
|
|
12
|
+
await access(join(targetDirectory, '.changeset/config.json'));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
17
|
+
return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
7
22
|
createRenovateWorkflowDefinition(),
|
|
23
|
+
{
|
|
24
|
+
relativePath: 'renovate.json5',
|
|
25
|
+
sourceUrl: new URL('./files/renovate.json5', import.meta.url),
|
|
26
|
+
mode: 'create-only',
|
|
27
|
+
},
|
|
8
28
|
];
|
|
9
29
|
function createWorkflowDefinition(relativePath, sourcePath) {
|
|
10
30
|
const sourceUrl = new URL(sourcePath, import.meta.url);
|
package/package.json
CHANGED