diff --git a/packages/core/AGENTS.md b/packages/core/AGENTS.md new file mode 100644 index 0000000..5c70d44 --- /dev/null +++ b/packages/core/AGENTS.md @@ -0,0 +1,48 @@ +# @repo/core — Clean Architecture Core + +Business logic package. All use cases, entities, interfaces, and DI live here. + +## Layers (dependencies point inward only) + +``` +entities/ → NOTHING (innermost, zero deps) +application/ → entities/ only +interface-adapters/→ application/, entities/ +infrastructure/ → application/, entities/, @repo/cms-client, external libs +di/ → all internal layers +``` + +## Import Rules + +| 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/* | + +## DI Resolution Table + +| Symbol | Interface | Production | Mock | +|---|---|---|---| +| IUsersRepository | IUsersRepository | PayloadUsersRepository (Plan 3) | MockUsersRepository | +| IArticlesRepository | IArticlesRepository | PayloadArticlesRepository (Plan 3) | MockArticlesRepository | +| IAuthenticationService | IAuthenticationService | BetterAuthService (future) | MockAuthenticationService | +| ITelemetryService | ITelemetryService | OTelSentryService (future) | MockTelemetryService | + +## 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` + +## Running Tests + +```bash +cd packages/core && pnpm vitest run +``` diff --git a/packages/core/src/application/AGENTS.md b/packages/core/src/application/AGENTS.md new file mode 100644 index 0000000..f6394f3 --- /dev/null +++ b/packages/core/src/application/AGENTS.md @@ -0,0 +1,28 @@ +# Application Layer — Use Cases + Interfaces + +## Rules + +- Imports from entities/ ONLY +- NEVER imports from infrastructure/ or interface-adapters/ +- Repository interfaces define data access contracts +- Service interfaces define external service contracts +- Use cases get dependencies via `getInjection()` — never direct import of implementations + +## Adding a New Use Case + +1. Create `src/application/use-cases/{domain}/{verb}-{noun}.use-case.ts` +2. Get dependencies via DI: `const repo = getInjection("IMyRepository")` +3. Implement business logic using entities and interfaces only +4. Write test in `tests/unit/use-cases/{domain}/` using `initializeContainer()`/`destroyContainer()` pattern + +## Adding a New Repository Interface + +1. Create `src/application/repositories/{name}.repository.interface.ts` +2. Define interface methods returning entity types +3. Export from `src/application/repositories/index.ts` +4. Create mock implementation in infrastructure/ +5. Register in DI container (add symbol, module binding) + +## Adding a New Service Interface + +Same as repository, but in `src/application/services/` diff --git a/packages/core/src/application/use-cases/auth/AGENTS.md b/packages/core/src/application/use-cases/auth/AGENTS.md new file mode 100644 index 0000000..890014c --- /dev/null +++ b/packages/core/src/application/use-cases/auth/AGENTS.md @@ -0,0 +1,30 @@ +# Auth Domain — Business Rules + +## Responsibility + +Authentication and authorization: sign-in, sign-up, sign-out, session management. + +## Business Rules + +- Passwords are hashed via IAuthenticationService (never stored plain) +- Sessions expire after 7 days (configured in mock, real impl may differ) +- Sign-up requires unique username +- Sign-in verifies password via IAuthenticationService.verifyPassword() +- Sign-out invalidates session and returns blank cookie + +## Error Cases + +- `AuthenticationError` — wrong credentials (sign-in) or username taken (sign-up) +- `UnauthenticatedError` — invalid/expired session + +## Dependencies + +- `IUsersRepository` — user lookup and creation +- `IAuthenticationService` — password hashing, session management + +## Adding a New Auth Use Case + +1. Create `{verb}-{noun}.use-case.ts` in this folder +2. Get deps via `getInjection("IUsersRepository")`, `getInjection("IAuthenticationService")` +3. Write test first in `tests/unit/use-cases/auth/` +4. Use `initializeContainer()`/`destroyContainer()` pattern in tests diff --git a/packages/core/src/application/use-cases/content/AGENTS.md b/packages/core/src/application/use-cases/content/AGENTS.md new file mode 100644 index 0000000..abf5459 --- /dev/null +++ b/packages/core/src/application/use-cases/content/AGENTS.md @@ -0,0 +1,29 @@ +# Content Domain — Business Rules + +## Responsibility + +Article management: creation, retrieval, publishing workflow. + +## Business Rules + +- Articles must have a title and content +- Slugs auto-generated from title if not provided +- New articles default to "draft" status +- Filtering by status, authorId supported +- Pagination via limit/offset + +## Error Cases + +- `NotFoundError` — article doesn't exist (future: update/delete) +- `UnauthorizedError` — user can't edit article (future) +- `InputParseError` — missing required fields (handled by controller) + +## Dependencies + +- `IArticlesRepository` — article CRUD operations + +## Adding a New Content Use Case + +1. Create `{verb}-{noun}.use-case.ts` in this folder +2. Get deps via `getInjection("IArticlesRepository")` +3. Write test first in `tests/unit/use-cases/content/` diff --git a/packages/core/src/di/AGENTS.md b/packages/core/src/di/AGENTS.md new file mode 100644 index 0000000..2fa39f3 --- /dev/null +++ b/packages/core/src/di/AGENTS.md @@ -0,0 +1,37 @@ +# DI — InversifyJS Container + +## Resolution Table + +| Symbol Key | Interface | Production | Mock | +|---|---|---|---| +| IUsersRepository | IUsersRepository | (future) | MockUsersRepository | +| IArticlesRepository | IArticlesRepository | (future) | MockArticlesRepository | +| IAuthenticationService | IAuthenticationService | (future) | MockAuthenticationService | +| ITelemetryService | ITelemetryService | (future) | MockTelemetryService | + +## How to Register a New Dependency + +1. Add Symbol to `types.ts` → `DI_SYMBOLS` +2. Add return type to `DI_RETURN_TYPES` interface +3. Create module in `modules/{domain}.module.ts` +4. Bind interface to implementation (production) and mock (test) +5. Load module in `container.ts` → `initializeContainer()` and `destroyContainer()` + +## Container Lifecycle + +- Production: `initializeContainer()` runs automatically (not in test env) +- Tests: Call `initializeContainer()` in `beforeEach`, `destroyContainer()` in `afterEach` +- Test environments swap to mock implementations via module bindings + +## DO NOT + +- Import from apps/* +- Import framework-specific code (Next.js, TanStack, etc.) +- Use the container outside of this package — expose via `getInjection()` only + +## tsconfig Requirements (DO NOT REMOVE) + +- `experimentalDecorators: true` +- `emitDecoratorMetadata: true` +- `types: ["reflect-metadata"]` +- `import "reflect-metadata"` at top of container.ts diff --git a/packages/core/src/entities/AGENTS.md b/packages/core/src/entities/AGENTS.md new file mode 100644 index 0000000..4f725b6 --- /dev/null +++ b/packages/core/src/entities/AGENTS.md @@ -0,0 +1,33 @@ +# Entities Layer — Innermost, Zero Dependencies + +## Rules + +- NEVER import from application/, infrastructure/, interface-adapters/, or di/ +- NEVER import external libraries except Zod (for schema validation) +- Everything here is pure — no side effects, no I/O, no async +- Models are Zod schemas with inferred TypeScript types +- Errors are custom Error subclasses with domain-specific semantics + +## Adding a New Model + +1. Create `src/entities/models/{name}.ts` +2. Define Zod schema and export inferred type: + ```typescript + import { z } from "zod"; + export const {name}Schema = z.object({ ... }); + export type {Name} = z.infer; + ``` +3. Export from `src/entities/models/index.ts` + +## Adding a New Error + +1. Create or edit `src/entities/errors/{domain}.ts` +2. Extend Error with constructor accepting message + options: + ```typescript + export class {Name}Error extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } + } + ``` +3. Export from `src/entities/errors/index.ts` diff --git a/packages/core/src/infrastructure/AGENTS.md b/packages/core/src/infrastructure/AGENTS.md new file mode 100644 index 0000000..25a186a --- /dev/null +++ b/packages/core/src/infrastructure/AGENTS.md @@ -0,0 +1,24 @@ +# Infrastructure Layer — Implementations + +## Rules + +- Implements interfaces from application/ +- Imports from application/ and entities/ +- NEVER imported by application/ or entities/ +- Can import external libraries (Drizzle, Better Auth, Sentry, etc.) +- Can import @repo/cms-client +- Always provide a mock implementation for every real implementation + +## Naming + +- Real: `{provider}-{name}.repository.ts` (e.g., `payload-users.repository.ts`) +- Mock: `mock-{name}.repository.ts` +- All implementations must use `@injectable()` decorator for InversifyJS + +## Adding a New Implementation + +1. Create `src/infrastructure/repositories/{provider}-{name}.repository.ts` +2. Implement the interface from application/ +3. Add `@injectable()` decorator +4. Create corresponding mock: `mock-{name}.repository.ts` +5. Register both in DI module (production binds real, test binds mock) diff --git a/packages/core/src/interface-adapters/controllers/AGENTS.md b/packages/core/src/interface-adapters/controllers/AGENTS.md new file mode 100644 index 0000000..9aa9e20 --- /dev/null +++ b/packages/core/src/interface-adapters/controllers/AGENTS.md @@ -0,0 +1,29 @@ +# Controllers — Interface Adapters + +## Rules + +- Controllers validate input using Zod schemas from entities/ +- Controllers call use cases — NEVER contain business logic themselves +- Controllers handle error mapping (domain errors to appropriate responses) +- Import from application/ and entities/ only +- NEVER import from infrastructure/ + +## Pattern + +```typescript +const inputSchema = z.object({ ... }); + +export async function myController(input: Partial>) { + const { data, error } = inputSchema.safeParse(input); + if (error) throw new InputParseError("Invalid data", { cause: error }); + return await myUseCase(data); +} +``` + +## Adding a New Controller + +1. Create `src/interface-adapters/controllers/{domain}/{name}.controller.ts` +2. Define Zod input schema +3. Validate input with `safeParse`, throw `InputParseError` on failure +4. Call use case and return result +5. Write test in `tests/unit/controllers/{domain}/`