Add a Clean Architecture feature
Touch rings from the inside out. Skip a ring only when it already exists.
Use the product’s language (Workspace, Issue, … in the issue tracker).
For package boundaries and code shapes, read the clean-architecture-app
skill files layers.md and templates.md.
Reference: @issue-tracker/* — flat IssueTrackerUseCases, camelCase names,
MemberUseCaseInput, requireActingUser, workspace-scoped SSE, MVVM UI.
Checklist
- Domain — handwritten types, invariants,
*_ERROR constants in
core/src/domain/<concept>.ts. Failures go through throwDomainError.
No I/O. No TypeBox.
- Use case —
type CreateFoo = UseCase<"createFoo", Input, Output> (camelCase
name literal). Handwritten Input/Output. Member commands use
MemberUseCaseInput<{ … }> unless the feature mints identity (no userId).
Extend UseCaseName and USE_CASE_NAMES. Import from @org/core/use-cases/*,
not the core root events module. Put query views next to the query contract.
- Validation (when JSON crosses a boundary) — TypeBox schema + exported
InputValidator in @org/validation/input; additionalProperties: false;
wire with withParsedExecute at the composition root only. Reuse domain
limit constants. Empty input still gets a validator.
- Event (only if other clients must refresh) — add a variant to the
closed union in
core/events.ts that includes the scope id (workspaceId,
and finer ids when useful: projectId, issueId). Publish an object literal
after run. Subscribers listen on workspaceId and re-run queries; ViewModels
may filter by event type and entity id. Skip the event when no other client
needs to refresh.
- Driven port — add or extend a port in
application (often
ports/repositories/). Persistence methods take tx first. Member commands
use RequestIdentity + requireActingUser(input, …) — not a second wire
actor parameter. Role-scoped products may use requireHost() / requirePlayer()
instead; do not mix both models on the same wire.
- Interactor —
createFoo(deps) in application; name matches the type
literal. One uow.run per execute; publish after run resolves.
- Tests — mock ports; cover identity (
requireActingUser or session),
domain validation, commit-then-publish, domain errors.
- Adapters — in-memory required; update SQLite (or other) adapter with the
same port signatures.
- Delivery — register in the composition root (
withParsedExecute if
needed). Extend the flat ProductUseCases object on ProductApp (one
property per use case). Extend dispatch switch and createProductClient in
client.ts (not http-client.ts). HTTP still takes { app: ProductApp }.
Wire names stay camelCase and match UseCaseName.
- UI — ViewModel method →
client.<useCase>.execute({ userId, … }).
For live data, client.events.subscribe(workspaceId, …) then background
refresh() on the active query (see live-events helpers). Do not import
application, infrastructure, or validation.
Mistakes to avoid
- TypeBox or
Static<> in core domain or use-case modules
- Driven ports in the use-case package
- Parse logic inside interactors instead of composition-root wrappers
- Nesting use cases instead of composing ports in one
run
- Kebab-case wire names that do not match the
UseCase name literal
forMember / nested facade on the wire when the app uses flat MemberUseCaseInput
- Trusting
input.userId without matching RequestIdentity
- Putting read-time joins on the write DTO unless that is an explicit domain decision
- Framework metadata as domain fields
- Implicit live updates; publish after commit, subscribe by scope, re-query
- Fetch, URLs, or session header logic in
client.ts
- Barrel
index.ts re-export hubs
- Implementing a ring the user has not asked for yet
1---2name: clean-architecture-feature3description: Adds one feature to a TypeScript Clean Architecture codebase by walking domain → use-case contract → interactor → driven port → adapter → delivery → UI. Use when adding a use case, entity, repository method, or application event to an existing core / application / validation / infrastructure workspace.4---56# Add a Clean Architecture feature78Touch rings from the inside out. Skip a ring only when it already exists.9Use the product’s language (`Workspace`, `Issue`, … in the issue tracker).1011For package boundaries and code shapes, read the `clean-architecture-app`12skill files `layers.md` and `templates.md`.1314**Reference:** `@issue-tracker/*` — flat `IssueTrackerUseCases`, camelCase names,15`MemberUseCaseInput`, `requireActingUser`, workspace-scoped SSE, MVVM UI.1617## Checklist18191. **Domain** — handwritten types, invariants, `*_ERROR` constants in20 `core/src/domain/<concept>.ts`. Failures go through `throwDomainError`.21 No I/O. No TypeBox.222. **Use case** — `type CreateFoo = UseCase<"createFoo", Input, Output>` (camelCase23 name literal). Handwritten `Input`/`Output`. Member commands use24 `MemberUseCaseInput<{ … }>` unless the feature mints identity (no `userId`).25 Extend `UseCaseName` and `USE_CASE_NAMES`. Import from `@org/core/use-cases/*`,26 not the core root events module. Put query views next to the query contract.273. **Validation** (when JSON crosses a boundary) — TypeBox schema + exported28 `InputValidator` in `@org/validation/input`; `additionalProperties: false`;29 wire with `withParsedExecute` at the composition root only. Reuse domain30 limit constants. Empty input still gets a validator.314. **Event** (only if other clients must refresh) — add a variant to the32 closed union in `core/events.ts` that includes the scope id (`workspaceId`,33 and finer ids when useful: `projectId`, `issueId`). Publish an object literal34 after `run`. Subscribers listen on `workspaceId` and re-run queries; ViewModels35 may filter by event type and entity id. Skip the event when no other client36 needs to refresh.375. **Driven port** — add or extend a port in `application` (often38 `ports/repositories/`). Persistence methods take `tx` first. Member commands39 use `RequestIdentity` + `requireActingUser(input, …)` — not a second wire40 actor parameter. Role-scoped products may use `requireHost()` / `requirePlayer()`41 instead; do not mix both models on the same wire.426. **Interactor** — `createFoo(deps)` in `application`; `name` matches the type43 literal. One `uow.run` per `execute`; publish after `run` resolves.447. **Tests** — mock ports; cover identity (`requireActingUser` or session),45 domain validation, commit-then-publish, domain errors.468. **Adapters** — in-memory required; update SQLite (or other) adapter with the47 same port signatures.489. **Delivery** — register in the composition root (`withParsedExecute` if49 needed). Extend the **flat** `ProductUseCases` object on `ProductApp` (one50 property per use case). Extend dispatch `switch` and `createProductClient` in51 `client.ts` (not `http-client.ts`). HTTP still takes `{ app: ProductApp }`.52 Wire names stay camelCase and match `UseCaseName`.5310. **UI** — ViewModel method → `client.<useCase>.execute({ userId, … })`.54 For live data, `client.events.subscribe(workspaceId, …)` then background55 `refresh()` on the active query (see `live-events` helpers). Do not import56 `application`, `infrastructure`, or `validation`.5758## Mistakes to avoid5960- TypeBox or `Static<>` in `core` domain or use-case modules61- Driven ports in the use-case package62- Parse logic inside interactors instead of composition-root wrappers63- Nesting use cases instead of composing ports in one `run`64- Kebab-case wire names that do not match the `UseCase` name literal65- `forMember` / nested facade on the wire when the app uses flat `MemberUseCaseInput`66- Trusting `input.userId` without matching `RequestIdentity`67- Putting read-time joins on the write DTO unless that is an explicit domain decision68- Framework metadata as domain fields69- Implicit live updates; publish after commit, subscribe by scope, re-query70- Fetch, URLs, or session header logic in `client.ts`71- Barrel `index.ts` re-export hubs72- Implementing a ring the user has not asked for yet