@ankhorage/devtools 1.14.3 → 1.14.5
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/agents/index.d.ts +1 -0
- package/dist/tools/agents/index.js +19 -4
- package/dist/tools/skills/assets/ankhorage-coding-rules/SKILL.md +4 -2
- package/dist/tools/skills/assets/ankhorage-project-structure/SKILL.md +74 -70
- package/dist/tools/skills/assets/hexagonal-architecture/SKILL.md +256 -52
- package/package.json +1 -1
|
@@ -2,5 +2,6 @@ export declare const agentsManagedFiles: readonly [{
|
|
|
2
2
|
readonly relativePath: "AGENTS.md";
|
|
3
3
|
readonly render: typeof renderAgentsFile;
|
|
4
4
|
}];
|
|
5
|
+
/*** Render mandatory repository instructions from the target package identity. */
|
|
5
6
|
declare function renderAgentsFile(targetDirectory: string): Promise<string>;
|
|
6
7
|
export {};
|
|
@@ -6,6 +6,7 @@ export const agentsManagedFiles = [
|
|
|
6
6
|
render: renderAgentsFile,
|
|
7
7
|
},
|
|
8
8
|
];
|
|
9
|
+
/*** Render mandatory repository instructions from the target package identity. */
|
|
9
10
|
async function renderAgentsFile(targetDirectory) {
|
|
10
11
|
const manifest = await readPackageManifest(targetDirectory);
|
|
11
12
|
const packageName = readNonEmptyString(manifest.name) ?? 'Package name not declared';
|
|
@@ -33,11 +34,25 @@ published public APIs and declared dependencies, never sibling source files.
|
|
|
33
34
|
Current-runtime error handling and canonical database or infrastructure migrations remain valid
|
|
34
35
|
when they support states that the current architecture can intentionally produce.
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Required repository instructions
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
Before changing any file, read this \`AGENTS.md\` completely and inspect \`.agents/skills/\`.
|
|
40
|
+
For every change, load and follow both baseline repository skills before making changes:
|
|
41
|
+
|
|
42
|
+
1. \`.agents/skills/ankhorage-coding-rules/SKILL.md\`
|
|
43
|
+
2. \`.agents/skills/ankhorage-project-structure/SKILL.md\`
|
|
44
|
+
|
|
45
|
+
Load and follow every additional repository-local skill relevant to the task, including skills
|
|
46
|
+
required by either baseline skill. Continue to follow these instructions and skills through
|
|
47
|
+
validation and delivery; do not substitute remembered, globally installed, or generic guidance for
|
|
48
|
+
the repository-local versions.
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
\`README.md\` and the \`paradox/\` documentation tree are generated by \`@ankhorage/paradox\`.
|
|
53
|
+
Never edit generated documentation directly. Update the owning Paradox \`/*** ... */\` comments in
|
|
54
|
+
\`src\`, including \`@readme\` tags where the content belongs in the README, then run \`bun docs\`
|
|
55
|
+
and commit the regenerated artifacts.
|
|
41
56
|
|
|
42
57
|
## Pull requests
|
|
43
58
|
|
|
@@ -38,8 +38,10 @@ exceptions or replaced by generic preferences from this skill.
|
|
|
38
38
|
validation pass.
|
|
39
39
|
- Resolve size, complexity, and related findings around cohesive responsibilities and clear data
|
|
40
40
|
flow. Do not split a function or file mechanically just to cross a threshold.
|
|
41
|
-
-
|
|
42
|
-
|
|
41
|
+
- Use functional programming by default: declare bindings with `const`, not `let`; write pure
|
|
42
|
+
functions with explicit inputs and outputs; and use immutable data and updates. Keep side effects
|
|
43
|
+
explicit and at system boundaries. Mutation or reassignment is allowed only for a clearly
|
|
44
|
+
justified boundary or demonstrated performance-critical path, and must remain locally contained.
|
|
43
45
|
- Keep one canonical implementation. Do not add compatibility aliases, dual paths, historical-state
|
|
44
46
|
fallbacks, or sibling-source imports.
|
|
45
47
|
|
|
@@ -1,41 +1,45 @@
|
|
|
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, Contracts ownership, 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
|
-
|
|
11
|
+
This skill applies to every Ankhorage repository.
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
exports, then load both required repository skills:
|
|
13
|
+
### Contracts repository profile
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
2. [Hexagonal Architecture](../hexagonal-architecture/SKILL.md)
|
|
15
|
+
If the current repository is `ankhorage/contracts`, apply only this profile:
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
- The repository may contain only portable, serializable contracts expressed as interfaces and
|
|
18
|
+
types. Every field must be serializable and reconstructable without executable behavior or
|
|
19
|
+
repository-local runtime objects. Do not add call signatures, function-valued properties,
|
|
20
|
+
functions, classes, constants, enums, mutable state, adapters, framework objects, or
|
|
21
|
+
implementation helpers.
|
|
22
|
+
- A contract belongs in this repository only when it is required across multiple repositories. A
|
|
23
|
+
current or coordinated change MUST identify at least two consuming repositories. Keep
|
|
24
|
+
single-repository types with their owning repository; anticipated reuse alone is not sufficient.
|
|
25
|
+
- Consumers import contracts through published public subpaths and declared dependencies, never
|
|
26
|
+
sibling source files or duplicated local declarations.
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
28
|
+
After enforcing this profile, stop before the remaining source-layout, implementation, utility,
|
|
29
|
+
and migration rules; they do not apply to `ankhorage/contracts`.
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## Required skills
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
Before structural work outside `ankhorage/contracts`, read the repository `AGENTS.md`, inspect its
|
|
34
|
+
source tree and public exports, then load the required Hexagonal Architecture skill from the
|
|
35
|
+
repository root. Do not resolve it relative to this skill's own installation location:
|
|
36
|
+
|
|
37
|
+
1. `<repo-root>/.agents/skills/hexagonal-architecture/SKILL.md`
|
|
33
38
|
|
|
34
|
-
##
|
|
39
|
+
## Required source layout
|
|
35
40
|
|
|
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.
|
|
41
|
+
- `examples/`: Repository-root folder in this standalone repository;
|
|
42
|
+
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
43
|
|
|
40
44
|
Each example lives in a named subdirectory, such as `examples/basic-usage/*.ts`. Do not put example
|
|
41
45
|
source files directly under `examples/`.
|
|
@@ -43,17 +47,29 @@ source files directly under `examples/`.
|
|
|
43
47
|
Test-only fixtures remain owned by the applicable test structure. Do not relabel fixtures as public
|
|
44
48
|
examples merely to bypass repository structure rules.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
- `src/cli/` must exist or have a concrete issue tracking the missing CLI commands;
|
|
51
|
+
CLI modules are thin inbound adapters: they parse input, invoke a feature use case, and render
|
|
52
|
+
output.
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
The filesystem below `src/cli/commands/` mirrors the public command path after the package prefix:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
ankh <package> <segment> ... <command>
|
|
58
|
+
-> src/cli/commands/<segment>/.../<command>.ts
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The package prefix is represented by the provider and is not repeated under `commands/`. Flags and
|
|
62
|
+
positional arguments do not affect this directory tree. Each command file follows the one-export
|
|
63
|
+
rule: `commands/projects/list.ts` exports `list` and owns only the command-specific input/output
|
|
64
|
+
mapping.
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
- `src/features/`: Lists the repository's actual product capabilities;
|
|
67
|
+
Technical categories are not features. Each feature owns its own hexagonal structure as needed,
|
|
68
|
+
following the required Hexagonal Architecture skill. Do not create empty layers.
|
|
55
69
|
|
|
56
70
|
```text
|
|
71
|
+
examples/
|
|
72
|
+
<example>/
|
|
57
73
|
src/
|
|
58
74
|
cli/
|
|
59
75
|
createCliProvider.ts
|
|
@@ -73,42 +89,33 @@ src/
|
|
|
73
89
|
inbound/
|
|
74
90
|
outbound/
|
|
75
91
|
composition/
|
|
92
|
+
constants/
|
|
93
|
+
<topic>.ts
|
|
76
94
|
utils/
|
|
77
95
|
types/
|
|
78
96
|
<topic>.ts
|
|
79
|
-
constants
|
|
97
|
+
constants/
|
|
98
|
+
<topic>.ts
|
|
80
99
|
utils/
|
|
81
100
|
```
|
|
82
101
|
|
|
83
102
|
Keep only deliberate package facades directly under `src/`. Public package subpaths must name their
|
|
84
103
|
explicit module in `package.json`; generic `index.ts` barrels are not public API exceptions.
|
|
85
104
|
|
|
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
|
|
105
|
+
## General Taxonomy
|
|
99
106
|
|
|
100
107
|
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 `
|
|
108
|
+
peer entities. For example, this is invalid because `other/` is not a color:
|
|
102
109
|
|
|
103
110
|
```text
|
|
104
111
|
colors/
|
|
105
112
|
red/
|
|
106
113
|
green/
|
|
107
114
|
blue/
|
|
108
|
-
|
|
115
|
+
other/
|
|
109
116
|
```
|
|
110
117
|
|
|
111
|
-
Resolve the ownership of `
|
|
118
|
+
Resolve the ownership of `other` and move it to the appropriate taxonomy. Use domain names for
|
|
112
119
|
features, not framework, transport, database, or generic technical names.
|
|
113
120
|
|
|
114
121
|
## Implementation modules
|
|
@@ -121,8 +128,7 @@ convenience barrels and must not expose private implementation details.
|
|
|
121
128
|
|
|
122
129
|
- `myFunction.ts` exports `myFunction`.
|
|
123
130
|
- `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.
|
|
131
|
+
- A public operation that is asynchronous or returns a `Promise` uses the `Async` suffix in both its filename and exported name.
|
|
126
132
|
|
|
127
133
|
Keep private helpers below that exported declaration when they are used only by that module.
|
|
128
134
|
Decide the owner of a reused function using the utility rules below, before creating another file.
|
|
@@ -132,17 +138,9 @@ Decide the owner of a reused function using the utility rules below, before crea
|
|
|
132
138
|
Choose type ownership by its production consumers, not by the number of textual references or
|
|
133
139
|
whether a barrel happens to re-export it:
|
|
134
140
|
|
|
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.
|
|
141
|
+
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.
|
|
142
|
+
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.
|
|
143
|
+
3. **Shared across repositories and serializable:** when at least two repositories require the same portable data declaration, it 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 non-serializable API types with their implementation-owning package and consume them through that package's public API. Keep framework-specific adapters separate from the portable shared contract.
|
|
146
144
|
|
|
147
145
|
Inspect published API declarations and real consumer imports before privatizing or relocating a
|
|
148
146
|
type. A public boundary type is not private just because only one implementation uses it locally.
|
|
@@ -161,11 +159,10 @@ Constants are static declarations, not utility implementations. Do not create on
|
|
|
161
159
|
constant per constant-named file under `utils/`.
|
|
162
160
|
|
|
163
161
|
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`.
|
|
162
|
+
2. **Reused only inside a feature:** group related constants in that feature's `constants/<topic>.ts`.
|
|
163
|
+
3. **Shared across features in one package:** group related package metadata, static policy values, and other constants in `src/constants/<topic>.ts`.
|
|
167
164
|
|
|
168
|
-
A `constants
|
|
165
|
+
A `constants/<topic>.ts` module may export multiple related constants. Keep it cohesive by ownership and
|
|
169
166
|
purpose; it is not a package-wide catch-all. Split constants when they have different owners, not
|
|
170
167
|
merely to create one file per export.
|
|
171
168
|
|
|
@@ -173,27 +170,34 @@ For example, Navigator's
|
|
|
173
170
|
[`src/utils/NAVIGATOR_PACKAGE_METADATA.ts`](https://github.com/ankhorage/navigator/blob/main/src/utils/NAVIGATOR_PACKAGE_METADATA.ts)
|
|
174
171
|
and
|
|
175
172
|
[`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`.
|
|
173
|
+
belong together in `ankhorage/navigator/src/constants/navigator.ts`.
|
|
177
174
|
|
|
178
175
|
## Utilities
|
|
179
176
|
|
|
180
177
|
`utils/` is the only utility directory name. Do not create `shared/`, `helper/`, `helpers/`,
|
|
181
178
|
`common/`, or equivalent catch-all folders. It is not a destination for every pure function or type.
|
|
182
179
|
|
|
180
|
+
Apply **reuse before implementation** and **shared by default** before choosing a local owner. For
|
|
181
|
+
every function that could reasonably be reused across repositories, you MUST first inspect the
|
|
182
|
+
published `@ankhorage/utility` public API and its owning topic. Reuse the existing export when its
|
|
183
|
+
semantics match. If the function is missing and is generic without product, manifest, or framework
|
|
184
|
+
policy, implement, test, and export it from the appropriate Utility topic, then consume that public
|
|
185
|
+
export through a declared dependency. Do not duplicate it locally or add a forwarding wrapper.
|
|
186
|
+
`isRecord` from `@ankhorage/utility/object` is one motivating example of this general rule, not a
|
|
187
|
+
special case.
|
|
188
|
+
|
|
183
189
|
- Used by one module: keep the helper private below its owning function.
|
|
184
190
|
- Reused only inside a feature: keep it in that feature's `utils/`.
|
|
185
191
|
- Shared across features but tied to this package's capability or policy: use `src/utils/`.
|
|
186
192
|
Navigator topology traversal or Expo Router-specific validation does not become a general utility
|
|
187
193
|
merely because several navigator features use it.
|
|
188
|
-
- Generally reusable
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
serialization. Do not copy a utility locally, create a forwarding wrapper, or change semantics
|
|
192
|
-
just to reuse a similarly named function.
|
|
194
|
+
- Generally reusable across repositories: it belongs in the canonical `@ankhorage/utility` topic
|
|
195
|
+
under the reuse-first rule above. Examples include generic string escaping or source-literal
|
|
196
|
+
serialization. Do not change semantics merely to reuse a similarly named function.
|
|
193
197
|
|
|
194
198
|
Separate the decisions for functions and types: reusable functions belong to Utility when general;
|
|
195
199
|
repo-local type groups belong to `src/types/`; repo-crossing types belong to Contracts. Respect
|
|
196
200
|
release boundaries and obtain approval for additional package changes when they exceed the task.
|
|
197
201
|
|
|
198
202
|
This skill defines the target architecture. Schedule repository migrations separately and in this
|
|
199
|
-
order: Studio, Deploy, Infra, Repository, Navigator.
|
|
203
|
+
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.
|
package/package.json
CHANGED