docs: extend all 18 AGENTS.md files with comprehensive context, code examples, and recipes
This commit is contained in:
@@ -1,48 +1,249 @@
|
||||
# @repo/core — Clean Architecture Core
|
||||
# @repo/core -- Clean Architecture Core Package
|
||||
|
||||
Business logic package. All use cases, entities, interfaces, and DI live here.
|
||||
This is the central business logic package. All domain entities, use cases, repository/service interfaces, controllers, and the InversifyJS DI container live here. Nothing in this package depends on any web framework (Next.js, TanStack, Payload). It is portable and testable in isolation.
|
||||
|
||||
## Layers (dependencies point inward only)
|
||||
**Package location:** `packages/core`
|
||||
**Entry point:** `src/index.ts`
|
||||
**Test runner:** Vitest (`pnpm vitest run` from this directory)
|
||||
|
||||
---
|
||||
|
||||
## Layer Diagram
|
||||
|
||||
Dependencies point inward (right to left). Outer layers depend on inner layers, never the reverse.
|
||||
|
||||
```
|
||||
entities/ → NOTHING (innermost, zero deps)
|
||||
application/ → entities/ only
|
||||
interface-adapters/→ application/, entities/
|
||||
infrastructure/ → application/, entities/, @repo/cms-client, external libs
|
||||
di/ → all internal layers
|
||||
+------------------------------------------------------------------+
|
||||
| di/ |
|
||||
| (wires everything together -- imports ALL internal layers) |
|
||||
+------------------------------------------------------------------+
|
||||
| | | |
|
||||
v v v v
|
||||
+----------------+ +----------------+ +----------------+ +-----------+
|
||||
| interface- | | infrastructure/| | application/ | | entities/ |
|
||||
| adapters/ | | (implements | | (use cases, | | (models, |
|
||||
| controllers/ | | interfaces | | repo/service | | errors) |
|
||||
| (validates | | with concrete | | interfaces) | | |
|
||||
| input, calls | | code) | | | | INNERMOST |
|
||||
| use cases) | | | | imports: | | zero deps |
|
||||
| | | imports: | | entities/ | | |
|
||||
| imports: | | application/ | | ONLY | | |
|
||||
| application/ | | entities/ | | | | |
|
||||
| entities/ | | @repo/ | | | | |
|
||||
| | | cms-client | | | | |
|
||||
+----------------+ +----------------+ +----------------+ +-----------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Import Rules
|
||||
|
||||
| Layer | Can import from | NEVER import from |
|
||||
| Layer | Can Import From | NEVER Import From |
|
||||
|---|---|---|
|
||||
| entities/ | NOTHING | Everything else |
|
||||
| application/ | entities/ only | infrastructure/, interface-adapters/ |
|
||||
| interface-adapters/ | application/, entities/ | infrastructure/ |
|
||||
| infrastructure/ | application/, entities/, @repo/cms-client | interface-adapters/, apps/* |
|
||||
| di/ | All internal layers | apps/* |
|
||||
| `entities/` | Nothing (Zod is the only external dependency) | `application/`, `infrastructure/`, `interface-adapters/`, `di/`, any `@repo/*`, any framework |
|
||||
| `application/` | `entities/` only | `infrastructure/`, `interface-adapters/`, `di/` (except `getInjection` from `di/container` in use cases) |
|
||||
| `interface-adapters/` | `application/` (use cases), `entities/` (types, errors) | `infrastructure/`, any `@repo/*` except via DI |
|
||||
| `infrastructure/` | `application/` (interfaces to implement), `entities/` (types), `@repo/cms-client` | `interface-adapters/`, apps/*, Next.js, TanStack |
|
||||
| `di/` | All internal layers (it wires them together) | apps/*, any framework package |
|
||||
|
||||
Note: Use cases in `application/` import `getInjection` from `di/container` to resolve dependencies at runtime. This is the one controlled exception to the "application never imports di" rule -- `getInjection` is a lookup function, not a concrete implementation.
|
||||
|
||||
---
|
||||
|
||||
## DI Resolution Table
|
||||
|
||||
| Symbol | Interface | Production | Mock |
|
||||
| Symbol Key | Interface | Production Implementation | Mock Implementation |
|
||||
|---|---|---|---|
|
||||
| IUsersRepository | IUsersRepository | PayloadUsersRepository (Plan 3) | MockUsersRepository |
|
||||
| IArticlesRepository | IArticlesRepository | PayloadArticlesRepository (Plan 3) | MockArticlesRepository |
|
||||
| IAuthenticationService | IAuthenticationService | BetterAuthService (future) | MockAuthenticationService |
|
||||
| ITelemetryService | ITelemetryService | OTelSentryService (future) | MockTelemetryService |
|
||||
| `IUsersRepository` | `IUsersRepository` (getUser, getUserByUsername, createUser) | PayloadUsersRepository (future -- will use `@repo/cms-client`) | `MockUsersRepository` (`infrastructure/repositories/mock-users.repository.ts`) |
|
||||
| `IArticlesRepository` | `IArticlesRepository` (getArticle, getArticles, createArticle, updateArticle) | PayloadArticlesRepository (future -- will use `@repo/cms-client`) | `MockArticlesRepository` (`infrastructure/repositories/mock-articles.repository.ts`) |
|
||||
| `IAuthenticationService` | `IAuthenticationService` (generateUserId, hashPassword, verifyPassword, validateSession, createSession, invalidateSession) | BetterAuthService (future) | `MockAuthenticationService` (`infrastructure/services/mock-auth.service.ts`) |
|
||||
| `ITelemetryService` | `ITelemetryService` (startSpan) | OTelSentryService (future) | `MockTelemetryService` (`infrastructure/services/mock-telemetry.service.ts`) |
|
||||
|
||||
Currently all modules bind mock implementations. When production implementations are added, the modules will conditionally bind based on environment or configuration.
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Models: `{name}.ts` with Zod schema + type export
|
||||
- Errors: `{domain}.ts` with Error subclasses
|
||||
- Interfaces: `{name}.repository.interface.ts` or `{name}.service.interface.ts`
|
||||
- Use cases: `{verb}-{noun}.use-case.ts`
|
||||
- Controllers: `{noun}.controller.ts`
|
||||
- Infra: `{provider}-{name}.repository.ts` or `mock-{name}.repository.ts`
|
||||
- DI modules: `{domain}.module.ts`
|
||||
| Type | Pattern | Example |
|
||||
|---|---|---|
|
||||
| Entity model | `{name}.ts` | `article.ts`, `user.ts` |
|
||||
| Entity error | `{domain}.ts` | `auth.ts`, `common.ts` |
|
||||
| Repository interface | `{name}.repository.interface.ts` | `users.repository.interface.ts` |
|
||||
| Service interface | `{name}.service.interface.ts` | `auth.service.interface.ts` |
|
||||
| Use case | `{verb}-{noun}.use-case.ts` | `sign-in.use-case.ts`, `create-article.use-case.ts` |
|
||||
| Controller | `{noun}.controller.ts` (may export multiple functions) | `articles.controller.ts` |
|
||||
| Production impl | `{provider}-{name}.repository.ts` | `payload-users.repository.ts` |
|
||||
| Mock impl | `mock-{name}.repository.ts` or `mock-{name}.service.ts` | `mock-users.repository.ts` |
|
||||
| DI module | `{domain}.module.ts` | `auth.module.ts`, `content.module.ts` |
|
||||
| Test file | `{name}.test.ts` matching the source file name | `sign-in.use-case.test.ts` |
|
||||
|
||||
---
|
||||
|
||||
## How to Add a New Dependency (5-Step Recipe)
|
||||
|
||||
This recipe adds a new repository or service interface to the DI container. For a complete feature, also follow the root `AGENTS.md` end-to-end recipe.
|
||||
|
||||
### Step 1: Define the interface in `application/`
|
||||
|
||||
Create `src/application/repositories/{name}.repository.interface.ts` or `src/application/services/{name}.service.interface.ts`:
|
||||
|
||||
```typescript
|
||||
import type { MyEntity } from "@/entities/models/my-entity";
|
||||
|
||||
export interface IMyRepository {
|
||||
findById(id: string): Promise<MyEntity | undefined>;
|
||||
findAll(): Promise<MyEntity[]>;
|
||||
create(input: MyEntity): Promise<MyEntity>;
|
||||
}
|
||||
```
|
||||
|
||||
Export from the appropriate barrel: `src/application/repositories/index.ts` or `src/application/services/index.ts`.
|
||||
|
||||
### Step 2: Add the DI symbol to `di/types.ts`
|
||||
|
||||
```typescript
|
||||
import type { IMyRepository } from "@/application/repositories/my.repository.interface";
|
||||
|
||||
export const DI_SYMBOLS = {
|
||||
// ...existing symbols...
|
||||
IMyRepository: Symbol.for("IMyRepository"),
|
||||
};
|
||||
|
||||
export interface DI_RETURN_TYPES {
|
||||
// ...existing types...
|
||||
IMyRepository: IMyRepository;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create mock implementation in `infrastructure/`
|
||||
|
||||
```typescript
|
||||
import { injectable } from "inversify";
|
||||
import type { IMyRepository } from "@/application/repositories/my.repository.interface";
|
||||
import type { MyEntity } from "@/entities/models/my-entity";
|
||||
|
||||
@injectable()
|
||||
export class MockMyRepository implements IMyRepository {
|
||||
private _items: MyEntity[] = [];
|
||||
|
||||
async findById(id: string): Promise<MyEntity | undefined> {
|
||||
return this._items.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
async findAll(): Promise<MyEntity[]> {
|
||||
return [...this._items];
|
||||
}
|
||||
|
||||
async create(input: MyEntity): Promise<MyEntity> {
|
||||
this._items.push(input);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Create or update DI module in `di/modules/`
|
||||
|
||||
```typescript
|
||||
import { ContainerModule, interfaces } from "inversify";
|
||||
import type { IMyRepository } from "@/application/repositories/my.repository.interface";
|
||||
import { MockMyRepository } from "@/infrastructure/repositories/mock-my.repository";
|
||||
import { DI_SYMBOLS } from "../types";
|
||||
|
||||
const initializeModule = (bind: interfaces.Bind) => {
|
||||
bind<IMyRepository>(DI_SYMBOLS.IMyRepository).to(MockMyRepository);
|
||||
};
|
||||
|
||||
export const MyModule = new ContainerModule(initializeModule);
|
||||
```
|
||||
|
||||
### Step 5: Load module in `di/container.ts`
|
||||
|
||||
```typescript
|
||||
import { MyModule } from "./modules/my.module";
|
||||
|
||||
export const initializeContainer = () => {
|
||||
// ...existing modules...
|
||||
ApplicationContainer.load(MyModule);
|
||||
};
|
||||
|
||||
export const destroyContainer = () => {
|
||||
// ...existing modules...
|
||||
ApplicationContainer.unload(MyModule);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Pattern
|
||||
|
||||
All tests follow this lifecycle pattern:
|
||||
|
||||
```typescript
|
||||
import "reflect-metadata";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { destroyContainer, initializeContainer } from "@/di/container";
|
||||
|
||||
beforeEach(() => {
|
||||
initializeContainer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
destroyContainer();
|
||||
});
|
||||
|
||||
describe("myUseCase", () => {
|
||||
it("does something", async () => {
|
||||
const result = await myUseCase(/* ... */);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Critical requirements:
|
||||
- `import "reflect-metadata"` MUST be the first import in every test file. InversifyJS decorators rely on runtime metadata reflection.
|
||||
- `initializeContainer()` loads all DI modules (binding mock implementations).
|
||||
- `destroyContainer()` unloads all modules, ensuring clean state between tests. Without this, Singleton-scoped mocks retain state across tests.
|
||||
|
||||
Test file locations mirror source structure:
|
||||
- `tests/unit/use-cases/{domain}/{name}.use-case.test.ts`
|
||||
- `tests/unit/controllers/{domain}/{name}.controller.test.ts`
|
||||
|
||||
---
|
||||
|
||||
## tsconfig Requirements
|
||||
|
||||
These settings in `tsconfig.json` are MANDATORY. Do not remove them:
|
||||
|
||||
| Setting | Why |
|
||||
|---|---|
|
||||
| `experimentalDecorators: true` | InversifyJS uses TypeScript decorators (`@injectable()`, `@inject()`) |
|
||||
| `emitDecoratorMetadata: true` | InversifyJS reads parameter type metadata at runtime for constructor injection |
|
||||
| `types: ["reflect-metadata", "node"]` | `reflect-metadata` polyfill must be globally available for decorator metadata |
|
||||
| Path alias `@/*` -> `./src/*` | All internal imports use `@/` prefix (e.g., `@/entities/models/user`) |
|
||||
|
||||
The base config comes from `@repo/typescript-config/base.json` which already includes `experimentalDecorators` and `emitDecoratorMetadata`. The core `tsconfig.json` extends it and adds the path alias and types.
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
cd packages/core && pnpm vitest run
|
||||
cd packages/core
|
||||
pnpm vitest run # Run all tests once
|
||||
pnpm vitest # Run in watch mode
|
||||
pnpm vitest run --reporter=verbose # Verbose output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- `src/entities/AGENTS.md` -- Entity models and error classes
|
||||
- `src/application/AGENTS.md` -- Use cases, repository/service interfaces
|
||||
- `src/infrastructure/AGENTS.md` -- Concrete implementations with `@injectable()`
|
||||
- `src/interface-adapters/controllers/AGENTS.md` -- Controller patterns
|
||||
- `src/di/AGENTS.md` -- DI container configuration and lifecycle
|
||||
- `src/application/use-cases/auth/AGENTS.md` -- Auth domain business rules
|
||||
- `src/application/use-cases/content/AGENTS.md` -- Content domain business rules
|
||||
- Root `AGENTS.md` -- Full end-to-end feature recipe and monorepo map
|
||||
|
||||
Reference in New Issue
Block a user