@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.
@@ -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
- Every Ankhorage repository follows this structure. It applies now to `ankhorage/studio`,
12
- `ankhorage/deploy`, `ankhorage/infra`, `ankhorage/repository`, and `ankhorage/navigator`.
9
+ ## Applicability
13
10
 
14
- ## Required skills
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
- 1. `.agents/skills/ankhorage-coding-rules/SKILL.md`
20
- 2. [Hexagonal Architecture](../hexagonal-architecture/SKILL.md)
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
- If `ankhorage-coding-rules` is missing or unreadable, stop immediately and report exactly:
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
- If `hexagonal-architecture` is missing or unreadable, stop immediately and report exactly:
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
- Cannot continue: the required repository skill `hexagonal-architecture` is missing or unreadable at `.agents/skills/hexagonal-architecture/SKILL.md`. Synchronize the repository skills from `@ankhorage/devtools` and retry.
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
- ## Repository-root examples
27
+ ## Required source layout
35
28
 
36
- `examples/` is a generally valid repository-root folder in every repository covered by this skill.
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
- ## Required source layout
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
- Every repository provides `src/features/`. It lists the repository's actual product capabilities;
49
- technical categories are not features. Each feature owns its own hexagonal structure as needed,
50
- following the required Hexagonal Architecture skill. Do not create empty layers.
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
- Every repository provides `src/cli/`, or has a concrete issue tracking the missing CLI commands.
53
- CLI modules are thin inbound adapters: they parse input, invoke a feature use case, and render
54
- output.
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.ts
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
- The filesystem below `src/cli/commands/` mirrors the public command path after the package prefix:
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 `otherFolder` is not a color:
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
- otherFolder/
103
+ other/
109
104
  ```
110
105
 
111
- Resolve the ownership of `otherFolder` and move it to the appropriate taxonomy. Use domain names for
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
- without `export`. Its private helpers can use the same local type. A test does not justify
137
- exporting an implementation-private type; test through the function boundary.
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.ts`.
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.ts` module may export multiple related constants. Keep it cohesive by ownership and
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
- Organize Ankhorage features with ports and adapters so domain policy stays independent from
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 keeps a feature's domain policy independent from its transport, framework,
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
- ## Feature boundaries
11
+ ## When to Use
20
12
 
21
- Apply this structure inside the owning `src/features/<feature>/` directory. A feature may contain
22
- only the layers it needs:
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
- <feature>/
26
- domain/
27
- application/
28
- ports/
29
- inbound/
30
- outbound/
31
- use-cases/
32
- adapters/
33
- inbound/
34
- outbound/
35
- composition/
36
- utils/
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
- - `domain/` contains entities, value objects, and business policy. It imports no framework,
40
- transport, database, or SDK code.
41
- - `application/` defines use cases and the ports they require. It orchestrates domain behavior and
42
- depends only inward.
43
- - `adapters/inbound/` converts CLI, HTTP, worker, or UI input to use-case input.
44
- - `adapters/outbound/` implements application ports for persistence, APIs, queues, clocks, and
45
- other external dependencies.
46
- - `composition/` explicitly wires concrete adapters to use cases.
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
- Dependency direction is always inward: adapters depend on application and domain; application
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
- ## Ports and use cases
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
- Define an inbound port when a feature needs a stable use-case boundary. Define an outbound port for
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
- Use cases accept plain inputs, enforce application invariants, coordinate domain behavior through
59
- ports, and return plain outputs. Keep protocol request objects, database rows, SDK clients, and
60
- transport error formats in adapters.
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
- Test domain rules without framework setup. Test use cases with fakes for outbound ports. Test
65
- adapters at their boundary with the real protocol or infrastructure they translate. Cover critical
66
- flows end-to-end through an inbound adapter, use case, and outbound adapter.
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
- ## Ankhorage conventions
266
+ ## Best Practices Checklist
69
267
 
70
- Follow `ankhorage-project-structure` for feature naming, one-export source modules, and `utils/`
71
- ownership. Do not introduce legacy paths, compatibility facades, dual implementations, or rollback
72
- switches while applying this architecture; use the current architecture directly.
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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @readme
3
+ */
4
+ {
5
+ $schema: 'https://docs.renovatebot.com/renovate-schema.json',
6
+ extends: ['github>ankhorage/renovate'],
7
+ }
@@ -1,2 +1,2 @@
1
1
  import type { ManagedFileDefinition } from '../shared/managedFiles.js';
2
- export declare const workflowManagedFiles: readonly [ManagedFileDefinition, ManagedFileDefinition, ManagedFileDefinition];
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
- createWorkflowDefinition('.github/workflows/release.yml', './files/release.yml'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",