First slice of the combined Plan 8 + Plan 9 doc-update pass: - CLAUDE.md Key Conventions: append schema-in-use-case, presenter, controller unknown input, feature-scoped tRPC error mapping, public surface split (./ui) - packages/core-shared/AGENTS.md: document defineErrorMiddleware export + t re-export from trpc/init - docs/superpowers/plans/2026-05-05-plan-8-*.md and matching spec: one-line note that some controller/router patterns shifted in Plan 9; link to the Plan 9 refactor log - docs/architecture/overview.md: data-flow box now shows xProcedure + xInputSchema + xOutputSchema.parse + presenter + middleware lanes; three explanatory paragraphs added (schemas, presenter, error mapping) - docs/architecture/dependency-flow.md: app-side ./ui subpath note, allowed/disallowed examples updated for Plan 9 paths Remaining doc-pass items (root AGENTS.md, per-feature AGENTS.md ×5, core-testing AGENTS.md, adding-a-feature.md, tdd-workflow.md, testing-strategy.md, vertical-feature-spec.md) follow in subsequent commits — to be dispatched in parallel.
1260 lines
54 KiB
Markdown
1260 lines
54 KiB
Markdown
# Plan 8 — Lazar Nikolov Pattern Conformance
|
|
|
|
> **Note (post-Plan-9, 2026-05-06):** Some controller / router / use-case patterns in this plan shifted in Plan 9. Use cases now own input + output schemas (`xInputSchema`, `xOutputSchema`); controllers receive `unknown` and run a top-level `function presenter` (Lazar pattern); routers consume `xProcedure` from each feature's `integrations/api/procedures.ts` instead of bare `publicProcedure`. See `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` and ADR-013 for the post-Plan-9 layout.
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Bring every feature in the monorepo into structural conformance with Lazar Nikolov's Clean Architecture pattern, while preserving our intentional vertical-feature design.
|
|
|
|
**Architecture:** Refactor each feature so use cases and controllers are factory functions with explicit DI; entities split into `models/` + `errors/` subdirs; mock files use `.mock.ts` suffix; one controller per use case; real Payload-backed `UsersRepository` and `AuthenticationService` added for auth; full Clean Architecture scaffold added for media. Intentional divergences (per-feature DI containers, inversify retained, colocated tests) documented in spec §4.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md` — read first if any task is unclear.
|
|
|
|
**Worktree:** Execute on `feature/lazar-conformance` in `.worktrees/lazar-conformance/`.
|
|
|
|
**Refactor changelog:** Maintain `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` throughout — every architectural change is captured for a follow-up doc-update pass. Update the changelog at the END of every task with: files renamed, files added, files deleted, pattern changes for the layer touched, and any new doc-update entries.
|
|
|
|
---
|
|
|
|
## Cross-cutting conventions (re-read at start of every task)
|
|
|
|
- **TDD always** — failing test, run, RED, implement, run, GREEN, refactor, commit.
|
|
- **Source files use relative imports**; test files use `@/` alias.
|
|
- **Inversify `.toDynamicValue` for factory bindings** — `bind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`.
|
|
- **`I*UseCase` / `I*Controller` type aliases** — every use case and controller exports these via `ReturnType<typeof xUseCase>`.
|
|
- **Commit per task** with clear message. Never bundle two tasks.
|
|
- **After each task** — `pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries`. All green.
|
|
- **Update the refactor changelog** at the end of each task before commit.
|
|
- **NEVER touch external docs** (CLAUDE.md, AGENTS.md, etc.) during this plan. Doc updates are a separate follow-up pass driven by the changelog.
|
|
|
|
---
|
|
|
|
### Task 1: Refactor changelog scaffold + Doc-update checklist
|
|
|
|
**Files:**
|
|
- Create: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`
|
|
|
|
- [ ] **Step 1: Create the changelog file with the standard sections**
|
|
|
|
`docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`:
|
|
|
|
```markdown
|
|
# Refactor Changelog — Lazar Pattern Conformance
|
|
|
|
**Started:** 2026-05-05
|
|
**Spec:** [2026-05-05-lazar-pattern-conformance-design.md](../specs/2026-05-05-lazar-pattern-conformance-design.md)
|
|
**Plan:** [2026-05-05-plan-8-lazar-conformance.md](../plans/2026-05-05-plan-8-lazar-conformance.md)
|
|
**Branch:** feature/lazar-conformance
|
|
|
|
This document captures every architectural change made during Plan 8
|
|
execution, organized by category. After the plan is merged, use the
|
|
"Doc update checklist" at the bottom to update external docs in a
|
|
single follow-up pass.
|
|
|
|
---
|
|
|
|
## 1. File renames (before → after)
|
|
|
|
(populated as work progresses)
|
|
|
|
## 2. Files added (with purpose)
|
|
|
|
(populated as work progresses)
|
|
|
|
## 3. Files deleted (with reason)
|
|
|
|
(populated as work progresses)
|
|
|
|
## 4. Pattern changes (code-level)
|
|
|
|
### 4.1 Use cases — factory function pattern
|
|
(populated when a use case is migrated)
|
|
|
|
### 4.2 Controllers — one per use case
|
|
(populated when controllers are split)
|
|
|
|
### 4.3 Entities split — models/ + errors/ subdirs
|
|
(populated when entities are reshaped)
|
|
|
|
## 5. DI changes
|
|
|
|
### 5.1 Inversify `.toDynamicValue` bindings
|
|
(populated when DI modules are updated)
|
|
|
|
### 5.2 Mock siblings registered as default bindings
|
|
(populated when modules are updated)
|
|
|
|
## 6. Test refactor patterns
|
|
|
|
### 6.1 Direct injection (no container rebinding)
|
|
(populated when tests are migrated)
|
|
|
|
## 7. Open issues / deferred decisions
|
|
|
|
(populated as encountered)
|
|
|
|
---
|
|
|
|
## Doc update checklist (deferred — run after merge)
|
|
|
|
- [ ] `CLAUDE.md` — Key Conventions section: update file path examples to use `entities/models/<x>.ts`, mention factory-function use cases
|
|
- [ ] `AGENTS.md` (root) — update Per-Package Conventions; add note about `I*UseCase` type aliases; update naming examples
|
|
- [ ] `docs/guides/adding-a-feature.md` — restructure to use factory-function pattern in every step; update file paths to new layout
|
|
- [ ] `docs/guides/tdd-workflow.md` — update "When to mock" section to show direct injection of mocks instead of container rebinding; update factory usage examples to reference new entity model paths
|
|
- [ ] `docs/guides/testing-strategy.md` — update Mocking section to remove DI-rebinding pattern; show direct factory injection
|
|
- [ ] `docs/architecture/vertical-feature-spec.md` — update §13 (testing) to reflect factory pattern; update file shape examples in §10
|
|
- [ ] `docs/architecture/overview.md` — update layer descriptions to mention factory functions
|
|
- [ ] `docs/architecture/dependency-flow.md` — verify dep flow still accurate with new DI bindings
|
|
- [ ] `docs/decisions/adr-012-lazar-conformance.md` — NEW ADR documenting the conformance decision + four intentional divergences (per-feature DI, inversify, colocated tests, no Sentry services)
|
|
- [ ] Per-feature `AGENTS.md` (auth/blog/media/marketing-pages/navigation) — update file path references; document factory pattern; update Tests section
|
|
- [ ] `packages/core-testing/AGENTS.md` — note that factories now live alongside entities at `entities/models/<x>.ts` paths
|
|
- [ ] `packages/auth/AGENTS.md` — document the new real PayloadUsersRepository + PayloadAuthenticationService
|
|
- [ ] `packages/media/AGENTS.md` — full rewrite — media now has all Clean Architecture layers
|
|
- [ ] Plan 7 plan/spec docs — add a note at the top that paths reference the pre-Plan-8 layout
|
|
|
|
---
|
|
|
|
## Notes for the doc-update pass author
|
|
|
|
- Replace any `entities/<x>.ts` reference with `entities/models/<x>.ts`
|
|
- Replace any `mock-<x>.repository.ts` reference with `<x>.repository.mock.ts`
|
|
- Replace any `<x>-repository.interface.ts` reference with `<x>.repository.interface.ts`
|
|
- Replace any `payload-<x>.repository.ts` reference with `<x>.repository.ts` (the real impl is now the canonical name)
|
|
- Add `I*UseCase` and `I*Controller` type alias examples
|
|
- The DI binding pattern code samples need to switch from `.to()` to `.toDynamicValue()` for use cases and controllers
|
|
- Test examples should show direct factory injection: `signInUseCase(mockUsers, mockAuth)(input)` instead of `container.get(SYMBOLS.ISignInUseCase)(input)`
|
|
```
|
|
|
|
- [ ] **Step 2: Verify file is readable**
|
|
|
|
```bash
|
|
ls -la docs/superpowers/refactor-logs/
|
|
cat docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md | head -20
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
mkdir -p docs/superpowers/refactor-logs
|
|
git add docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md
|
|
git commit -m "docs(refactor-log): scaffold Lazar conformance refactor changelog
|
|
|
|
Empty section template plus the full doc-update checklist that the
|
|
follow-up pass will work through after the refactor is merged. Spec:
|
|
docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md §10."
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Foundation — entities split into models/ + errors/ (all 5 features)
|
|
|
|
**Files (per feature):**
|
|
- Move: `src/entities/<x>.ts` → `src/entities/models/<x>.ts`
|
|
- Split: `src/entities/errors.ts` → `src/entities/errors/<domain>.ts` + `src/entities/errors/common.ts`
|
|
- Update: every import that references the moved/split files
|
|
|
|
For each feature, list of moves:
|
|
|
|
**auth:**
|
|
- `entities/user.ts` → `entities/models/user.ts`
|
|
- `entities/session.ts` → `entities/models/session.ts`
|
|
- `entities/cookie.ts` → `entities/models/cookie.ts`
|
|
- `entities/errors.ts` → split into `entities/errors/auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError) + `entities/errors/common.ts` (InputParseError)
|
|
- Delete: `entities/errors.ts` (replaced by subdir contents)
|
|
|
|
**blog:**
|
|
- `entities/article.ts` → `entities/models/article.ts`
|
|
- `entities/errors.ts` → split into `entities/errors/article.ts` (ArticleNotFoundError) + `entities/errors/common.ts` (InputParseError)
|
|
|
|
**marketing-pages:**
|
|
- `entities/page.ts` → `entities/models/page.ts`
|
|
- `entities/site-settings.ts` → `entities/models/site-settings.ts`
|
|
- `entities/errors.ts` → split into `entities/errors/page.ts` (PageNotFoundError) + `entities/errors/common.ts` (InputParseError)
|
|
|
|
**navigation:**
|
|
- `entities/header.ts` → `entities/models/header.ts`
|
|
- `entities/errors.ts` → split into `entities/errors/header.ts` (HeaderNotFoundError if it exists, otherwise just empty placeholder) + `entities/errors/common.ts` (InputParseError)
|
|
|
|
**media:**
|
|
- No entities yet (created in Task 9). Skip for now.
|
|
|
|
- [ ] **Step 1: Per feature, create the new directory structure first**
|
|
|
|
```bash
|
|
for feat in auth blog marketing-pages navigation; do
|
|
mkdir -p packages/$feat/src/entities/models packages/$feat/src/entities/errors
|
|
done
|
|
```
|
|
|
|
- [ ] **Step 2: For each feature, move entity files using `git mv`**
|
|
|
|
```bash
|
|
# auth
|
|
git mv packages/auth/src/entities/user.ts packages/auth/src/entities/models/user.ts
|
|
git mv packages/auth/src/entities/session.ts packages/auth/src/entities/models/session.ts
|
|
git mv packages/auth/src/entities/cookie.ts packages/auth/src/entities/models/cookie.ts
|
|
# blog
|
|
git mv packages/blog/src/entities/article.ts packages/blog/src/entities/models/article.ts
|
|
# marketing-pages
|
|
git mv packages/marketing-pages/src/entities/page.ts packages/marketing-pages/src/entities/models/page.ts
|
|
git mv packages/marketing-pages/src/entities/site-settings.ts packages/marketing-pages/src/entities/models/site-settings.ts
|
|
# navigation
|
|
git mv packages/navigation/src/entities/header.ts packages/navigation/src/entities/models/header.ts
|
|
```
|
|
|
|
- [ ] **Step 3: For each feature, split errors.ts into domain + common files**
|
|
|
|
Read each `errors.ts` first to know what classes are inside. Then create domain-grouped files.
|
|
|
|
For auth — read `packages/auth/src/entities/errors.ts`. Create:
|
|
|
|
`packages/auth/src/entities/errors/auth.ts`:
|
|
```typescript
|
|
export class AuthenticationError extends Error {
|
|
constructor(message: string, options?: ErrorOptions) {
|
|
super(message, options);
|
|
}
|
|
}
|
|
|
|
export class UnauthenticatedError extends Error {
|
|
constructor(message: string, options?: ErrorOptions) {
|
|
super(message, options);
|
|
}
|
|
}
|
|
|
|
export class UnauthorizedError extends Error {
|
|
constructor(message: string, options?: ErrorOptions) {
|
|
super(message, options);
|
|
}
|
|
}
|
|
```
|
|
|
|
`packages/auth/src/entities/errors/common.ts`:
|
|
```typescript
|
|
export class InputParseError extends Error {
|
|
constructor(message: string, options?: ErrorOptions) {
|
|
super(message, options);
|
|
}
|
|
}
|
|
```
|
|
|
|
Adjust per feature based on what errors actually exist in each `errors.ts`.
|
|
|
|
- [ ] **Step 4: Delete old `entities/errors.ts`**
|
|
|
|
```bash
|
|
git rm packages/auth/src/entities/errors.ts
|
|
git rm packages/blog/src/entities/errors.ts
|
|
git rm packages/marketing-pages/src/entities/errors.ts
|
|
git rm packages/navigation/src/entities/errors.ts
|
|
```
|
|
|
|
- [ ] **Step 5: Update all imports referencing the old paths**
|
|
|
|
Use grep to find consumers, then update one at a time.
|
|
|
|
```bash
|
|
grep -rln 'from "../../entities/errors"\|from "../../entities/article"\|from "../../entities/user"\|from "../../entities/session"\|from "../../entities/cookie"\|from "../../entities/page"\|from "../../entities/site-settings"\|from "../../entities/header"' packages/
|
|
```
|
|
|
|
Update each occurrence to the new path:
|
|
- `entities/errors` → `entities/errors/<domain>` or `entities/errors/common` (depending on which class is imported)
|
|
- `entities/<x>` → `entities/models/<x>`
|
|
|
|
Common imports to fix:
|
|
- `entities/errors` (anywhere it's imported) — split into either `errors/auth`, `errors/article`, `errors/page`, `errors/header` (for domain errors) or `errors/common` (for InputParseError)
|
|
- `entities/article` → `entities/models/article`
|
|
- `entities/user` → `entities/models/user`
|
|
- … etc
|
|
|
|
**Critical:** check `__factories__/`, `__contracts__/`, `*.test.ts`, repositories, use cases, controllers, integrations/api/router.ts, integrations/cms/collections/. Every import must resolve.
|
|
|
|
- [ ] **Step 6: Verify**
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm typecheck # all type imports resolve
|
|
pnpm lint
|
|
pnpm test # all 244 tests still pass
|
|
pnpm turbo boundaries
|
|
```
|
|
|
|
If any test fails because the factory or contract imports the wrong path, fix the import, NOT the structure.
|
|
|
|
- [ ] **Step 7: Update refactor changelog**
|
|
|
|
Edit `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`. Under §1 File renames, append:
|
|
```
|
|
### Task 2: Entities split
|
|
|
|
- packages/auth/src/entities/user.ts → packages/auth/src/entities/models/user.ts
|
|
- packages/auth/src/entities/session.ts → packages/auth/src/entities/models/session.ts
|
|
- packages/auth/src/entities/cookie.ts → packages/auth/src/entities/models/cookie.ts
|
|
- packages/auth/src/entities/errors.ts → packages/auth/src/entities/errors/auth.ts + packages/auth/src/entities/errors/common.ts (split)
|
|
- packages/blog/src/entities/article.ts → packages/blog/src/entities/models/article.ts
|
|
- packages/blog/src/entities/errors.ts → packages/blog/src/entities/errors/article.ts + common.ts
|
|
- packages/marketing-pages/src/entities/page.ts → packages/marketing-pages/src/entities/models/page.ts
|
|
- packages/marketing-pages/src/entities/site-settings.ts → packages/marketing-pages/src/entities/models/site-settings.ts
|
|
- packages/marketing-pages/src/entities/errors.ts → packages/marketing-pages/src/entities/errors/page.ts + common.ts
|
|
- packages/navigation/src/entities/header.ts → packages/navigation/src/entities/models/header.ts
|
|
- packages/navigation/src/entities/errors.ts → packages/navigation/src/entities/errors/header.ts + common.ts
|
|
```
|
|
|
|
Under §4.3 Pattern changes — Entities split:
|
|
```
|
|
- Entities now live at `entities/models/<x>.ts` (Zod schema + type)
|
|
- Errors live at `entities/errors/<domain>.ts` (domain-specific classes) + `entities/errors/common.ts` (InputParseError)
|
|
- Each feature owns its own InputParseError (duplicated, ~6 lines per feature)
|
|
```
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```bash
|
|
git add packages/ docs/superpowers/refactor-logs/
|
|
git commit -m "refactor(features): split entities into models/ + errors/ subdirs
|
|
|
|
All 5 features (auth, blog, marketing-pages, navigation; media has no
|
|
entities yet) now follow Lazar's pattern:
|
|
- entities/<x>.ts → entities/models/<x>.ts
|
|
- entities/errors.ts → entities/errors/<domain>.ts + errors/common.ts
|
|
|
|
Updates all import paths across factories, contracts, tests, use cases,
|
|
controllers, repositories, integrations.
|
|
|
|
Refactor log: docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md
|
|
Spec: §5, §9.3"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Foundation — file renames (mock + payload + interface)
|
|
|
|
**Files (per feature):**
|
|
|
|
For each repository:
|
|
- `infrastructure/repositories/mock-<noun>.repository.ts` → `<noun>.repository.mock.ts`
|
|
- `infrastructure/repositories/payload-<noun>.repository.ts` → `<noun>.repository.ts`
|
|
- `application/repositories/<noun>-repository.interface.ts` → `<noun>.repository.interface.ts`
|
|
|
|
For each service (auth only currently):
|
|
- `infrastructure/services/mock-<noun>.service.ts` → `<noun>.service.mock.ts`
|
|
- `application/services/<noun>-service.interface.ts` → `<noun>.service.interface.ts`
|
|
|
|
Specific renames:
|
|
|
|
**auth:**
|
|
- `mock-users.repository.ts` → `users.repository.mock.ts`
|
|
- `users-repository.interface.ts` → `users.repository.interface.ts`
|
|
- `mock-authentication.service.ts` → `authentication.service.mock.ts`
|
|
- `authentication-service.interface.ts` → `authentication.service.interface.ts`
|
|
(Note: real `users.repository.ts` and `authentication.service.ts` are added in Task 5.)
|
|
|
|
**blog:**
|
|
- `mock-articles.repository.ts` → `articles.repository.mock.ts`
|
|
- `payload-articles.repository.ts` → `articles.repository.ts`
|
|
- `articles-repository.interface.ts` → `articles.repository.interface.ts`
|
|
|
|
**marketing-pages:**
|
|
- `mock-pages.repository.ts` → `pages.repository.mock.ts`
|
|
- `payload-pages.repository.ts` → `pages.repository.ts`
|
|
- `pages-repository.interface.ts` → `pages.repository.interface.ts`
|
|
- `mock-site-settings.repository.ts` → `site-settings.repository.mock.ts`
|
|
- `payload-site-settings.repository.ts` → `site-settings.repository.ts`
|
|
- `site-settings-repository.interface.ts` → `site-settings.repository.interface.ts`
|
|
|
|
**navigation:**
|
|
- `mock-header.repository.ts` → `header.repository.mock.ts`
|
|
- `payload-header.repository.ts` → `header.repository.ts`
|
|
- `header-repository.interface.ts` → `header.repository.interface.ts`
|
|
|
|
**media:** none (Task 9 creates these from scratch).
|
|
|
|
- [ ] **Step 1: Use `git mv` for every rename**
|
|
|
|
```bash
|
|
# auth
|
|
git mv packages/auth/src/infrastructure/repositories/mock-users.repository.ts packages/auth/src/infrastructure/repositories/users.repository.mock.ts
|
|
git mv packages/auth/src/application/repositories/users-repository.interface.ts packages/auth/src/application/repositories/users.repository.interface.ts
|
|
git mv packages/auth/src/infrastructure/services/mock-authentication.service.ts packages/auth/src/infrastructure/services/authentication.service.mock.ts
|
|
git mv packages/auth/src/application/services/authentication-service.interface.ts packages/auth/src/application/services/authentication.service.interface.ts
|
|
# blog
|
|
git mv packages/blog/src/infrastructure/repositories/mock-articles.repository.ts packages/blog/src/infrastructure/repositories/articles.repository.mock.ts
|
|
git mv packages/blog/src/infrastructure/repositories/payload-articles.repository.ts packages/blog/src/infrastructure/repositories/articles.repository.ts
|
|
git mv packages/blog/src/application/repositories/articles-repository.interface.ts packages/blog/src/application/repositories/articles.repository.interface.ts
|
|
# marketing-pages
|
|
git mv packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.ts packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts
|
|
git mv packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.ts packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts
|
|
git mv packages/marketing-pages/src/application/repositories/pages-repository.interface.ts packages/marketing-pages/src/application/repositories/pages.repository.interface.ts
|
|
git mv packages/marketing-pages/src/infrastructure/repositories/mock-site-settings.repository.ts packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts
|
|
git mv packages/marketing-pages/src/infrastructure/repositories/payload-site-settings.repository.ts packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts
|
|
git mv packages/marketing-pages/src/application/repositories/site-settings-repository.interface.ts packages/marketing-pages/src/application/repositories/site-settings.repository.interface.ts
|
|
# navigation
|
|
git mv packages/navigation/src/infrastructure/repositories/mock-header.repository.ts packages/navigation/src/infrastructure/repositories/header.repository.mock.ts
|
|
git mv packages/navigation/src/infrastructure/repositories/payload-header.repository.ts packages/navigation/src/infrastructure/repositories/header.repository.ts
|
|
git mv packages/navigation/src/application/repositories/header-repository.interface.ts packages/navigation/src/application/repositories/header.repository.interface.ts
|
|
```
|
|
|
|
- [ ] **Step 2: Update all imports referencing the old paths**
|
|
|
|
```bash
|
|
grep -rln 'mock-users.repository\|mock-articles.repository\|mock-pages.repository\|mock-site-settings.repository\|mock-header.repository\|mock-authentication.service\|payload-articles.repository\|payload-pages.repository\|payload-site-settings.repository\|payload-header.repository\|users-repository.interface\|articles-repository.interface\|pages-repository.interface\|site-settings-repository.interface\|header-repository.interface\|authentication-service.interface' packages/
|
|
```
|
|
|
|
Update each — replace the old path with the new path. Watch:
|
|
- DI module bindings (still bind to the same class names, just import from new files)
|
|
- Test files that import the mock or real impl
|
|
- Contract suites that import the interface
|
|
- Factories that import nothing from these (probably none)
|
|
|
|
- [ ] **Step 3: Verify class names unchanged**
|
|
|
|
The class names stay (`MockUsersRepository`, `PayloadUsersRepository` → wait, `PayloadUsersRepository` doesn't exist in auth yet; for the others, the class name changes from e.g. `PayloadArticlesRepository` to `ArticlesRepository`).
|
|
|
|
**Decision:** also rename the classes for consistency. So:
|
|
- `class PayloadArticlesRepository` → `class ArticlesRepository`
|
|
- `class PayloadPagesRepository` → `class PagesRepository`
|
|
- `class PayloadSiteSettingsRepository` → `class SiteSettingsRepository`
|
|
- `class PayloadHeaderRepository` → `class HeaderRepository`
|
|
|
|
The `Mock` prefix stays: `MockArticlesRepository`, `MockUsersRepository`, etc.
|
|
|
|
Update:
|
|
1. Class name in the file
|
|
2. All consumers (DI module, test, contract `buildSubject`)
|
|
|
|
- [ ] **Step 4: Verify**
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm typecheck
|
|
pnpm lint
|
|
pnpm test
|
|
pnpm turbo boundaries
|
|
```
|
|
|
|
If anything fails, the import or class-rename is the issue.
|
|
|
|
- [ ] **Step 5: Update refactor changelog**
|
|
|
|
Append to `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` §1:
|
|
|
|
```
|
|
### Task 3: File and class renames
|
|
|
|
File renames (16 files):
|
|
- packages/auth/src/infrastructure/repositories/mock-users.repository.ts → users.repository.mock.ts
|
|
- packages/auth/src/application/repositories/users-repository.interface.ts → users.repository.interface.ts
|
|
- packages/auth/src/infrastructure/services/mock-authentication.service.ts → authentication.service.mock.ts
|
|
- packages/auth/src/application/services/authentication-service.interface.ts → authentication.service.interface.ts
|
|
- packages/blog/src/infrastructure/repositories/mock-articles.repository.ts → articles.repository.mock.ts
|
|
- packages/blog/src/infrastructure/repositories/payload-articles.repository.ts → articles.repository.ts
|
|
- packages/blog/src/application/repositories/articles-repository.interface.ts → articles.repository.interface.ts
|
|
- packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.ts → pages.repository.mock.ts
|
|
- packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.ts → pages.repository.ts
|
|
- packages/marketing-pages/src/application/repositories/pages-repository.interface.ts → pages.repository.interface.ts
|
|
- packages/marketing-pages/src/infrastructure/repositories/mock-site-settings.repository.ts → site-settings.repository.mock.ts
|
|
- packages/marketing-pages/src/infrastructure/repositories/payload-site-settings.repository.ts → site-settings.repository.ts
|
|
- packages/marketing-pages/src/application/repositories/site-settings-repository.interface.ts → site-settings.repository.interface.ts
|
|
- packages/navigation/src/infrastructure/repositories/mock-header.repository.ts → header.repository.mock.ts
|
|
- packages/navigation/src/infrastructure/repositories/payload-header.repository.ts → header.repository.ts
|
|
- packages/navigation/src/application/repositories/header-repository.interface.ts → header.repository.interface.ts
|
|
|
|
Class renames:
|
|
- PayloadArticlesRepository → ArticlesRepository
|
|
- PayloadPagesRepository → PagesRepository
|
|
- PayloadSiteSettingsRepository → SiteSettingsRepository
|
|
- PayloadHeaderRepository → HeaderRepository
|
|
- (Mock* class names unchanged)
|
|
```
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add packages/ docs/superpowers/refactor-logs/
|
|
git commit -m "refactor(features): rename mock/payload/interface files per Lazar pattern
|
|
|
|
Convention now: <name>.repository.{ts,mock.ts,interface.ts}.
|
|
Renames .mock prefix to .mock suffix; drops .payload prefix from real
|
|
impls (canonical name = real impl); dot-separates the .repository
|
|
qualifier in interface filenames. Class names follow suit:
|
|
PayloadXRepository → XRepository; Mock* unchanged.
|
|
|
|
Refactor log: §1, §3
|
|
Spec: §9.1"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Refactor `auth` to factory functions + add real Payload impls
|
|
|
|
**Files (auth):**
|
|
- Modify: every use case (`sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts`) — convert to factory function, export `I*UseCase` type
|
|
- Modify: every controller (`sign-in.controller.ts`, `sign-up.controller.ts`, `sign-out.controller.ts`) — convert to factory function, export `I*Controller` type
|
|
- Modify: `di/symbols.ts` — add use case + controller symbols
|
|
- Modify: `di/module.ts` — add `.toDynamicValue()` bindings for use cases and controllers
|
|
- Create: `infrastructure/repositories/users.repository.ts` — real Payload-backed `UsersRepository`
|
|
- Create: `infrastructure/services/authentication.service.ts` — real `AuthenticationService` using Payload's auth API
|
|
- Modify: `di/bind-production.ts` — swap mocks for real impls
|
|
- Modify: `integrations/api/router.ts` — controllers resolved via container.get()
|
|
- Modify: tests — direct factory injection (no container.get)
|
|
|
|
#### Step 1-N: per use case TDD migration
|
|
|
|
For each of `sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts`:
|
|
|
|
- [ ] **Step 1: Update the use case test FIRST** to use direct factory injection (RED initially because factory doesn't exist yet)
|
|
|
|
`packages/auth/src/application/use-cases/sign-in.use-case.test.ts`:
|
|
```typescript
|
|
import { describe, it, expect } from "vitest";
|
|
import { signInUseCase } from "@/application/use-cases/sign-in.use-case";
|
|
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
|
|
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
|
|
import { AuthenticationError } from "@/entities/errors/auth";
|
|
import { userFactory } from "@/__factories__/user.factory";
|
|
|
|
describe("signInUseCase", () => {
|
|
it("creates a session for valid credentials", async () => {
|
|
const users = new MockUsersRepository();
|
|
const auth = new MockAuthenticationService(users);
|
|
const seedUser = userFactory.build({ username: "alice" });
|
|
await users.createUser(seedUser);
|
|
|
|
const useCase = signInUseCase(users, auth);
|
|
const result = await useCase({ username: "alice", password: seedUser.passwordHash.replace("hashed_", "") });
|
|
|
|
expect(result.session).toBeDefined();
|
|
expect(result.cookie.value).toBe(result.session.id);
|
|
});
|
|
|
|
it("throws AuthenticationError when user does not exist", async () => {
|
|
const users = new MockUsersRepository();
|
|
const auth = new MockAuthenticationService(users);
|
|
const useCase = signInUseCase(users, auth);
|
|
|
|
await expect(useCase({ username: "missing", password: "x" })).rejects.toThrow(AuthenticationError);
|
|
});
|
|
|
|
it("throws AuthenticationError when password is incorrect", async () => {
|
|
const users = new MockUsersRepository();
|
|
const auth = new MockAuthenticationService(users);
|
|
await users.createUser(userFactory.build({ username: "alice" }));
|
|
|
|
const useCase = signInUseCase(users, auth);
|
|
await expect(useCase({ username: "alice", password: "wrong" })).rejects.toThrow(AuthenticationError);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run test — RED (factory function not exported yet)**
|
|
|
|
```bash
|
|
pnpm test --filter @repo/auth -- sign-in.use-case
|
|
```
|
|
|
|
Expected: FAIL — `signInUseCase is not a function` or similar.
|
|
|
|
- [ ] **Step 3: Convert the use case to a factory function**
|
|
|
|
`packages/auth/src/application/use-cases/sign-in.use-case.ts`:
|
|
```typescript
|
|
import { AuthenticationError } from "../../entities/errors/auth";
|
|
import type { Cookie } from "../../entities/models/cookie";
|
|
import type { Session } from "../../entities/models/session";
|
|
import type { IUsersRepository } from "../repositories/users.repository.interface";
|
|
import type { IAuthenticationService } from "../services/authentication.service.interface";
|
|
|
|
export type ISignInUseCase = ReturnType<typeof signInUseCase>;
|
|
|
|
export const signInUseCase =
|
|
(usersRepository: IUsersRepository, authenticationService: IAuthenticationService) =>
|
|
async (input: { username: string; password: string }): Promise<{ session: Session; cookie: Cookie }> => {
|
|
const existingUser = await usersRepository.getUserByUsername(input.username);
|
|
if (!existingUser) {
|
|
throw new AuthenticationError("User does not exist");
|
|
}
|
|
const validPassword = await authenticationService.verifyPassword(
|
|
existingUser.passwordHash,
|
|
input.password,
|
|
);
|
|
if (!validPassword) {
|
|
throw new AuthenticationError("Incorrect username or password");
|
|
}
|
|
return await authenticationService.createSession(existingUser);
|
|
};
|
|
```
|
|
|
|
- [ ] **Step 4: Run test — GREEN**
|
|
|
|
```bash
|
|
pnpm test --filter @repo/auth -- sign-in.use-case
|
|
```
|
|
|
|
- [ ] **Step 5: Repeat for `sign-up.use-case.ts` and `sign-out.use-case.ts`**
|
|
|
|
Same pattern. Read the existing use case, identify its dependencies, write the test first with direct injection, then convert to factory.
|
|
|
|
#### Step N+1: Convert controllers similarly
|
|
|
|
For each of `sign-in.controller.ts`, `sign-up.controller.ts`, `sign-out.controller.ts`:
|
|
|
|
- [ ] **Step a: Update the controller test to use direct injection**
|
|
|
|
`packages/auth/src/interface-adapters/controllers/sign-in.controller.test.ts`:
|
|
```typescript
|
|
import { describe, it, expect } from "vitest";
|
|
import { signInController } from "@/interface-adapters/controllers/sign-in.controller";
|
|
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
|
|
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
|
|
import { signInUseCase } from "@/application/use-cases/sign-in.use-case";
|
|
import { InputParseError } from "@/entities/errors/common";
|
|
import { userFactory } from "@/__factories__/user.factory";
|
|
|
|
describe("signInController", () => {
|
|
it("returns the cookie on successful sign-in", async () => {
|
|
const users = new MockUsersRepository();
|
|
const auth = new MockAuthenticationService(users);
|
|
const seedUser = userFactory.build({ username: "alice" });
|
|
await users.createUser(seedUser);
|
|
|
|
const useCase = signInUseCase(users, auth);
|
|
const controller = signInController(useCase);
|
|
|
|
const cookie = await controller({
|
|
username: "alice",
|
|
password: seedUser.passwordHash.replace("hashed_", ""),
|
|
});
|
|
|
|
expect(cookie).toBeDefined();
|
|
expect(cookie.name).toBeTruthy();
|
|
});
|
|
|
|
it("throws InputParseError on invalid input", async () => {
|
|
const users = new MockUsersRepository();
|
|
const auth = new MockAuthenticationService(users);
|
|
const useCase = signInUseCase(users, auth);
|
|
const controller = signInController(useCase);
|
|
|
|
await expect(controller({ username: "x" })).rejects.toThrow(InputParseError);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step b: Run — RED**
|
|
|
|
- [ ] **Step c: Convert the controller**
|
|
|
|
`packages/auth/src/interface-adapters/controllers/sign-in.controller.ts`:
|
|
```typescript
|
|
import { z } from "zod";
|
|
import { InputParseError } from "../../entities/errors/common";
|
|
import type { Cookie } from "../../entities/models/cookie";
|
|
import type { ISignInUseCase } from "../../application/use-cases/sign-in.use-case";
|
|
|
|
const inputSchema = z.object({
|
|
username: z.string().min(3).max(31),
|
|
password: z.string().min(6).max(255),
|
|
});
|
|
|
|
export type ISignInController = ReturnType<typeof signInController>;
|
|
|
|
export const signInController =
|
|
(signInUseCase: ISignInUseCase) =>
|
|
async (input: Partial<z.infer<typeof inputSchema>>): Promise<Cookie> => {
|
|
const parsed = inputSchema.safeParse(input);
|
|
if (!parsed.success) {
|
|
throw new InputParseError("Invalid sign-in input", { cause: parsed.error });
|
|
}
|
|
const { cookie } = await signInUseCase(parsed.data);
|
|
return cookie;
|
|
};
|
|
```
|
|
|
|
- [ ] **Step d: GREEN**
|
|
|
|
#### Step N+2: Update DI
|
|
|
|
- [ ] **Step a: Add use case and controller symbols**
|
|
|
|
`packages/auth/src/di/symbols.ts` — add to `AUTH_SYMBOLS`:
|
|
```typescript
|
|
export const AUTH_SYMBOLS = {
|
|
IUsersRepository: Symbol.for("IUsersRepository"),
|
|
IAuthenticationService: Symbol.for("IAuthenticationService"),
|
|
// Use cases
|
|
ISignInUseCase: Symbol.for("ISignInUseCase"),
|
|
ISignUpUseCase: Symbol.for("ISignUpUseCase"),
|
|
ISignOutUseCase: Symbol.for("ISignOutUseCase"),
|
|
// Controllers
|
|
ISignInController: Symbol.for("ISignInController"),
|
|
ISignUpController: Symbol.for("ISignUpController"),
|
|
ISignOutController: Symbol.for("ISignOutController"),
|
|
};
|
|
```
|
|
|
|
- [ ] **Step b: Add `.toDynamicValue()` bindings**
|
|
|
|
`packages/auth/src/di/module.ts`:
|
|
```typescript
|
|
import { ContainerModule } from "inversify";
|
|
import { AUTH_SYMBOLS } from "./symbols";
|
|
import { MockUsersRepository } from "../infrastructure/repositories/users.repository.mock";
|
|
import { MockAuthenticationService } from "../infrastructure/services/authentication.service.mock";
|
|
import { signInUseCase, type ISignInUseCase } from "../application/use-cases/sign-in.use-case";
|
|
import { signUpUseCase, type ISignUpUseCase } from "../application/use-cases/sign-up.use-case";
|
|
import { signOutUseCase, type ISignOutUseCase } from "../application/use-cases/sign-out.use-case";
|
|
import { signInController, type ISignInController } from "../interface-adapters/controllers/sign-in.controller";
|
|
import { signUpController, type ISignUpController } from "../interface-adapters/controllers/sign-up.controller";
|
|
import { signOutController, type ISignOutController } from "../interface-adapters/controllers/sign-out.controller";
|
|
import type { IUsersRepository } from "../application/repositories/users.repository.interface";
|
|
import type { IAuthenticationService } from "../application/services/authentication.service.interface";
|
|
|
|
export const authModule = new ContainerModule((bind) => {
|
|
bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).to(MockUsersRepository);
|
|
bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService).to(MockAuthenticationService);
|
|
|
|
bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
|
|
signInUseCase(
|
|
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
|
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
|
),
|
|
);
|
|
bind<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase).toDynamicValue((ctx) =>
|
|
signUpUseCase(
|
|
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
|
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
|
),
|
|
);
|
|
bind<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase).toDynamicValue((ctx) =>
|
|
signOutUseCase(ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)),
|
|
);
|
|
|
|
bind<ISignInController>(AUTH_SYMBOLS.ISignInController).toDynamicValue((ctx) =>
|
|
signInController(ctx.container.get<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase)),
|
|
);
|
|
bind<ISignUpController>(AUTH_SYMBOLS.ISignUpController).toDynamicValue((ctx) =>
|
|
signUpController(ctx.container.get<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase)),
|
|
);
|
|
bind<ISignOutController>(AUTH_SYMBOLS.ISignOutController).toDynamicValue((ctx) =>
|
|
signOutController(ctx.container.get<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase)),
|
|
);
|
|
});
|
|
```
|
|
|
|
#### Step N+3: Update integrations/api/router.ts
|
|
|
|
- [ ] **Step a: Update tRPC router to resolve controllers via DI**
|
|
|
|
`packages/auth/src/integrations/api/router.ts`:
|
|
```typescript
|
|
import { router, publicProcedure } from "@repo/core-shared/trpc/init";
|
|
import { z } from "zod";
|
|
import { authContainer } from "../../di/container";
|
|
import { AUTH_SYMBOLS } from "../../di/symbols";
|
|
import type { ISignInController } from "../../interface-adapters/controllers/sign-in.controller";
|
|
import type { ISignUpController } from "../../interface-adapters/controllers/sign-up.controller";
|
|
import type { ISignOutController } from "../../interface-adapters/controllers/sign-out.controller";
|
|
|
|
const signInInput = z.object({ username: z.string(), password: z.string() });
|
|
const signUpInput = z.object({ username: z.string(), password: z.string() });
|
|
const signOutInput = z.object({ sessionId: z.string() });
|
|
|
|
export const authRouter = router({
|
|
signIn: publicProcedure.input(signInInput).mutation(async ({ input }) => {
|
|
const ctrl = authContainer.get<ISignInController>(AUTH_SYMBOLS.ISignInController);
|
|
return ctrl(input);
|
|
}),
|
|
signUp: publicProcedure.input(signUpInput).mutation(async ({ input }) => {
|
|
const ctrl = authContainer.get<ISignUpController>(AUTH_SYMBOLS.ISignUpController);
|
|
return ctrl(input);
|
|
}),
|
|
signOut: publicProcedure.input(signOutInput).mutation(async ({ input }) => {
|
|
const ctrl = authContainer.get<ISignOutController>(AUTH_SYMBOLS.ISignOutController);
|
|
return ctrl(input);
|
|
}),
|
|
});
|
|
```
|
|
|
|
#### Step N+4: Add real PayloadUsersRepository + AuthenticationService
|
|
|
|
- [ ] **Step a: Write failing test for `UsersRepository` (Payload-backed)**
|
|
|
|
`packages/auth/src/infrastructure/repositories/users.repository.test.ts`:
|
|
```typescript
|
|
import { describe, vi, beforeEach } from "vitest";
|
|
import { UsersRepository } from "./users.repository";
|
|
import { usersRepositoryContract } from "../../__contracts__/users-repository.contract";
|
|
import { stubPayloadConfig } from "@repo/core-testing/payload/stub-config";
|
|
|
|
vi.mock("payload", () => ({ getPayload: vi.fn() }));
|
|
|
|
function buildPayloadStub() {
|
|
const store = new Map();
|
|
return {
|
|
create: vi.fn(async ({ data }) => { store.set(data.id, data); return data; }),
|
|
find: vi.fn(async ({ where }) => {
|
|
const all = Array.from(store.values());
|
|
if (where?.username?.equals) return { docs: all.filter((u) => u.username === where.username.equals) };
|
|
return { docs: all };
|
|
}),
|
|
findByID: vi.fn(async ({ id }) => store.get(id) ?? null),
|
|
};
|
|
}
|
|
|
|
describe("UsersRepository", () => {
|
|
describe("contract", () => {
|
|
usersRepositoryContract.run(async () => {
|
|
const stub = buildPayloadStub();
|
|
const { getPayload } = await import("payload");
|
|
(getPayload as ReturnType<typeof vi.fn>).mockResolvedValue(stub);
|
|
return new UsersRepository(stubPayloadConfig);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step b: RED**
|
|
|
|
- [ ] **Step c: Implement UsersRepository**
|
|
|
|
`packages/auth/src/infrastructure/repositories/users.repository.ts`:
|
|
```typescript
|
|
import { getPayload } from "payload";
|
|
import type { Config } from "payload";
|
|
import type { IUsersRepository } from "../../application/repositories/users.repository.interface";
|
|
import { type User, userSchema } from "../../entities/models/user";
|
|
|
|
export class UsersRepository implements IUsersRepository {
|
|
constructor(private config: Config) {}
|
|
|
|
async getUser(id: string): Promise<User | undefined> {
|
|
const payload = await getPayload({ config: this.config });
|
|
const result = await payload.findByID({ collection: "users", id, overrideAccess: true });
|
|
return result ? userSchema.parse(this.toDomain(result)) : undefined;
|
|
}
|
|
|
|
async getUserByUsername(username: string): Promise<User | undefined> {
|
|
const payload = await getPayload({ config: this.config });
|
|
const { docs } = await payload.find({
|
|
collection: "users",
|
|
where: { username: { equals: username } },
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
});
|
|
return docs[0] ? userSchema.parse(this.toDomain(docs[0])) : undefined;
|
|
}
|
|
|
|
async createUser(input: User): Promise<User> {
|
|
const payload = await getPayload({ config: this.config });
|
|
const created = await payload.create({
|
|
collection: "users",
|
|
data: { id: input.id, username: input.username, passwordHash: input.passwordHash },
|
|
overrideAccess: true,
|
|
});
|
|
return userSchema.parse(this.toDomain(created));
|
|
}
|
|
|
|
private toDomain(doc: Record<string, unknown>): User {
|
|
return {
|
|
id: doc.id as string,
|
|
username: doc.username as string,
|
|
passwordHash: doc.passwordHash as string,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step d: GREEN**
|
|
|
|
- [ ] **Step e: Write test + impl for AuthenticationService (similar pattern, using Payload's auth API)**
|
|
|
|
This is more involved. Read the Mock impl as a guide. The real impl should:
|
|
- `verifyPassword`: use Payload's `local.login` (or bcrypt directly) — depends on Payload's auth strategy
|
|
- `createSession`: use Payload's `local.login` to issue a session token; map to `Session` and `Cookie`
|
|
- `invalidateSession`: clear session via Payload
|
|
- `validateSession`: use Payload's `local.findByID` on sessions or rely on session token
|
|
|
|
Document any deviation in the changelog under Open issues if Payload's auth API doesn't map cleanly. If too complex, make it a stub that always throws "not implemented" with a TODO comment, and document as deferred work in the changelog. Don't block this task on perfect Payload auth integration.
|
|
|
|
- [ ] **Step f: Update `bind-production.ts` to swap mocks for real impls**
|
|
|
|
`packages/auth/src/di/bind-production.ts`:
|
|
```typescript
|
|
import type { Config } from "payload";
|
|
import { authContainer } from "./container";
|
|
import { AUTH_SYMBOLS } from "./symbols";
|
|
import { UsersRepository } from "../infrastructure/repositories/users.repository";
|
|
import { AuthenticationService } from "../infrastructure/services/authentication.service";
|
|
import type { IUsersRepository } from "../application/repositories/users.repository.interface";
|
|
import type { IAuthenticationService } from "../application/services/authentication.service.interface";
|
|
|
|
let bound = false;
|
|
|
|
export function bindProductionAuth(config: Config): void {
|
|
if (bound) return;
|
|
bound = true;
|
|
|
|
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
|
|
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
|
|
}
|
|
authContainer
|
|
.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
|
|
.toConstantValue(new UsersRepository(config));
|
|
|
|
if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) {
|
|
authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService);
|
|
}
|
|
authContainer
|
|
.bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)
|
|
.toConstantValue(new AuthenticationService(config));
|
|
}
|
|
```
|
|
|
|
#### Step N+5: Verify
|
|
|
|
- [ ] **Verification**
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm test --filter @repo/auth # all auth tests pass with new pattern
|
|
pnpm test # global suite still green
|
|
pnpm typecheck
|
|
pnpm lint
|
|
pnpm turbo boundaries
|
|
```
|
|
|
|
#### Step N+6: Update changelog
|
|
|
|
- [ ] **Update changelog with Task 4 entries**:
|
|
|
|
Under §2 Files added: list `users.repository.ts`, `authentication.service.ts`, controller test files (if new), use case test files (if new).
|
|
|
|
Under §4.1 Use cases — factory function pattern:
|
|
```
|
|
- All auth use cases (sign-in, sign-up, sign-out) refactored to factory function:
|
|
`(deps) => async (input) => result`
|
|
- Each exports `I*UseCase = ReturnType<typeof xUseCase>`
|
|
- Use cases NO LONGER call `authContainer.get()` — deps are passed in
|
|
- Tests construct mocks directly: `signInUseCase(mockUsers, mockAuth)(input)`
|
|
```
|
|
|
|
Under §4.2 Controllers — one per use case:
|
|
```
|
|
- auth controllers already split (sign-in, sign-up, sign-out — one file each)
|
|
- Refactored to factory function: `(useCase) => async (input) => result`
|
|
- Each exports `I*Controller = ReturnType<typeof xController>`
|
|
```
|
|
|
|
Under §5.1 DI bindings:
|
|
```
|
|
- AUTH_SYMBOLS expanded with use case and controller keys
|
|
- Use case and controller bindings use `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))`
|
|
- tRPC router resolves controllers via container.get() instead of calling use cases directly
|
|
```
|
|
|
|
Under §6.1 Test refactor patterns:
|
|
```
|
|
- Use case + controller tests now construct mocks and inject directly:
|
|
`const useCase = signInUseCase(mockUsers, mockAuth); await useCase(input);`
|
|
- No more container.get() in tests
|
|
- No more rebinding in beforeEach
|
|
```
|
|
|
|
#### Step N+7: Commit
|
|
|
|
- [ ] **Commit**
|
|
|
|
```bash
|
|
git add packages/auth docs/superpowers/refactor-logs/
|
|
git commit -m "refactor(auth): factory-style use cases + controllers + real Payload impls
|
|
|
|
- Use cases (sign-in, sign-up, sign-out) → factory functions with I*UseCase aliases
|
|
- Controllers → factory functions with I*Controller aliases
|
|
- DI symbols + module updated with .toDynamicValue() bindings for factories
|
|
- New: real UsersRepository (Payload-backed)
|
|
- New: real AuthenticationService (Payload-backed; some methods may be deferred — see refactor log)
|
|
- bindProductionAuth swaps both mocks for real impls
|
|
- Tests refactored to construct mocks and inject directly (no container)
|
|
|
|
Refactor log: §2, §4.1, §4.2, §5.1, §6.1
|
|
Spec: §6.1, §7"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Refactor `blog` to factory functions + add getArticleBySlug use case
|
|
|
|
**Files:**
|
|
- Modify: `application/use-cases/get-articles.use-case.ts`, `create-article.use-case.ts` — convert to factory
|
|
- Create: `application/use-cases/get-article-by-slug.use-case.ts` (factory)
|
|
- Modify: `interface-adapters/controllers/articles.controller.ts` — DELETE this file
|
|
- Create: `interface-adapters/controllers/get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts` (factory each)
|
|
- Modify: `di/symbols.ts` — add use case + controller symbols
|
|
- Modify: `di/module.ts` — add `.toDynamicValue()` bindings
|
|
- Modify: `integrations/api/router.ts` — resolve controllers via container
|
|
- Modify: tests for use cases + controllers
|
|
|
|
Use Task 4 as the template. Same pattern, different feature.
|
|
|
|
- [ ] **Step 1: For each existing use case (get-articles, create-article), update test → RED → convert to factory → GREEN**
|
|
|
|
- [ ] **Step 2: Create the new `get-article-by-slug` use case TDD-style**
|
|
|
|
Test:
|
|
```typescript
|
|
// packages/blog/src/application/use-cases/get-article-by-slug.use-case.test.ts
|
|
import { describe, it, expect } from "vitest";
|
|
import { getArticleBySlugUseCase } from "@/application/use-cases/get-article-by-slug.use-case";
|
|
import { MockArticlesRepository } from "@/infrastructure/repositories/articles.repository.mock";
|
|
import { ArticleNotFoundError } from "@/entities/errors/article";
|
|
import { articleFactory } from "@/__factories__/article.factory";
|
|
|
|
describe("getArticleBySlugUseCase", () => {
|
|
it("returns the article when slug exists", async () => {
|
|
const repo = new MockArticlesRepository();
|
|
const seed = articleFactory.build({ slug: "test-slug" });
|
|
await repo.createArticle(seed);
|
|
|
|
const useCase = getArticleBySlugUseCase(repo);
|
|
const result = await useCase({ slug: "test-slug" });
|
|
|
|
expect(result?.slug).toBe("test-slug");
|
|
});
|
|
|
|
it("throws ArticleNotFoundError when slug is missing", async () => {
|
|
const repo = new MockArticlesRepository();
|
|
const useCase = getArticleBySlugUseCase(repo);
|
|
await expect(useCase({ slug: "does-not-exist" })).rejects.toThrow(ArticleNotFoundError);
|
|
});
|
|
});
|
|
```
|
|
|
|
Implementation:
|
|
```typescript
|
|
// packages/blog/src/application/use-cases/get-article-by-slug.use-case.ts
|
|
import { ArticleNotFoundError } from "../../entities/errors/article";
|
|
import type { Article } from "../../entities/models/article";
|
|
import type { IArticlesRepository } from "../repositories/articles.repository.interface";
|
|
|
|
export type IGetArticleBySlugUseCase = ReturnType<typeof getArticleBySlugUseCase>;
|
|
|
|
export const getArticleBySlugUseCase =
|
|
(articlesRepository: IArticlesRepository) =>
|
|
async (input: { slug: string }): Promise<Article> => {
|
|
const article = await articlesRepository.getArticleBySlug(input.slug);
|
|
if (!article) {
|
|
throw new ArticleNotFoundError(`Article with slug "${input.slug}" not found`);
|
|
}
|
|
return article;
|
|
};
|
|
```
|
|
|
|
- [ ] **Step 3: Convert each controller to its own factory file**
|
|
|
|
Delete the old `articles.controller.ts`. Create three new files: `get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts`. Each is a factory function consuming its corresponding use case.
|
|
|
|
- [ ] **Step 4: Update DI symbols, module, router**
|
|
|
|
Same pattern as Task 4 — add symbols, add `.toDynamicValue()` bindings, update tRPC router to resolve via container.
|
|
|
|
- [ ] **Step 5: Verify**
|
|
|
|
- [ ] **Step 6: Update changelog**
|
|
|
|
Under §2 Files added: `get-article-by-slug.use-case.ts` and three new controller files. Under §3 Files deleted: `articles.controller.ts`. Under §4.1, §4.2, §5.1: confirm patterns applied.
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```
|
|
refactor(blog): factory-style use cases + per-use-case controllers + getArticleBySlug
|
|
|
|
- Use cases (create-article, get-articles, get-article-by-slug NEW) → factory functions
|
|
- Controllers split: articles.controller.ts → 3 single-responsibility files
|
|
- DI module wires factories with .toDynamicValue()
|
|
- tRPC router resolves controllers via container
|
|
|
|
Refactor log: §2, §3, §4.1, §4.2, §5.1
|
|
Spec: §6.2"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Refactor `marketing-pages` to factory functions
|
|
|
|
Same pattern as Task 5. Use cases: `get-page-by-slug`, `get-site-settings`. Controllers: split `pages.controller.ts` into per-use-case files.
|
|
|
|
Each TDD'd, single-commit.
|
|
|
|
Commit message: `refactor(marketing-pages): factory-style use cases + per-use-case controllers`
|
|
|
|
---
|
|
|
|
### Task 7: Refactor `navigation` to factory functions
|
|
|
|
Same pattern. Single use case (`get-header`) and single controller (`get-header.controller.ts` — already isomorphic). Just convert to factory style + add type aliases + DI updates.
|
|
|
|
Commit message: `refactor(navigation): factory-style use case + controller`
|
|
|
|
---
|
|
|
|
### Task 8: Scaffold `media` as a full Clean Architecture feature
|
|
|
|
This is the largest task. Read media's current state:
|
|
|
|
```bash
|
|
ls packages/media/src/
|
|
ls packages/media/src/integrations/cms/collections/
|
|
```
|
|
|
|
Today: only `integrations/cms/collections/media.ts` exists. Need to add the full template:
|
|
|
|
**Files to create:**
|
|
- `src/entities/models/media.ts` — Zod schema (Media type — id, alt, url, filename, mimeType, filesize, width, height) — adapt from existing factory
|
|
- `src/entities/errors/media.ts` (MediaNotFoundError)
|
|
- `src/entities/errors/common.ts` (InputParseError)
|
|
- `src/application/repositories/media.repository.interface.ts` — IMediaRepository: `getMedia(id)`, `getMediaById(id)`, `listMedia(opts)`, `deleteMedia(id)`
|
|
- `src/application/use-cases/get-media.use-case.ts` (factory)
|
|
- `src/application/use-cases/list-media.use-case.ts` (factory)
|
|
- `src/application/use-cases/delete-media.use-case.ts` (factory)
|
|
- `src/infrastructure/repositories/media.repository.ts` (Payload-backed)
|
|
- `src/infrastructure/repositories/media.repository.mock.ts`
|
|
- `src/interface-adapters/controllers/get-media.controller.ts`
|
|
- `src/interface-adapters/controllers/list-media.controller.ts`
|
|
- `src/interface-adapters/controllers/delete-media.controller.ts`
|
|
- `src/di/symbols.ts` (MEDIA_SYMBOLS)
|
|
- `src/di/module.ts`
|
|
- `src/di/container.ts`
|
|
- `src/di/bind-production.ts`
|
|
- `src/integrations/api/router.ts` (mediaRouter)
|
|
- `src/integrations/api/index.ts`
|
|
- `src/__factories__/media.factory.ts` (already exists — adapt to use new entity model path)
|
|
- `src/__contracts__/media-repository.contract.ts` (NEW)
|
|
- `src/index.ts` — public exports
|
|
- `tests/media.feature.test.ts` (feature integration)
|
|
|
|
**Modify:**
|
|
- `packages/media/package.json` — add inversify and reflect-metadata deps; expose `./api` and `./di/bind-production` exports
|
|
- `packages/core-api/src/root.ts` — add `media: mediaRouter` to appRouter
|
|
- `apps/web-next/src/server/bind-production.ts` — call `bindProductionMedia(config)` at boot
|
|
- `tsconfig.base.json` — add `@repo/media/api` and `@repo/media/di/bind-production` aliases
|
|
|
|
TDD each component. Commit at end.
|
|
|
|
Commit message: `feat(media): full Clean Architecture scaffold`
|
|
|
|
---
|
|
|
|
### Task 9: Update factories + contracts to point at new entity paths
|
|
|
|
After tasks 2-8, the entity paths have changed (`models/<x>.ts` instead of `<x>.ts`). Verify all factory imports + contract imports are correctly pointing at the new paths.
|
|
|
|
```bash
|
|
grep -rn "from.*entities/article\b" packages/blog/src/__factories__ packages/blog/src/__contracts__
|
|
```
|
|
|
|
Each grep should return zero results because the paths are now `entities/models/article`.
|
|
|
|
Run all tests to confirm.
|
|
|
|
Commit message: `chore(features): align factory + contract imports with entities/models/* paths` (only if there are actual changes; this task may be a no-op if Task 2 already updated all imports).
|
|
|
|
---
|
|
|
|
### Task 10: Final verification + refactor changelog completion
|
|
|
|
- [ ] **Run full validation**
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm typecheck
|
|
pnpm lint
|
|
pnpm test
|
|
pnpm turbo boundaries
|
|
pnpm build
|
|
```
|
|
|
|
All green.
|
|
|
|
- [ ] **Verify file layout matches spec §5 template for every feature**
|
|
|
|
```bash
|
|
for feat in auth blog marketing-pages navigation media; do
|
|
echo "=== $feat ==="
|
|
find packages/$feat/src -type f -name "*.ts" | sort
|
|
done
|
|
```
|
|
|
|
Compare against §5. Flag any deviations in the changelog.
|
|
|
|
- [ ] **Verify no `entities/<x>.ts` exists at root** (only `entities/models/<x>.ts` and `entities/errors/<x>.ts`)
|
|
|
|
```bash
|
|
find packages/*/src/entities -maxdepth 1 -type f -name "*.ts"
|
|
```
|
|
|
|
Should be empty (any file at this level is a violation).
|
|
|
|
- [ ] **Verify no `mock-` prefixed files exist**
|
|
|
|
```bash
|
|
find packages -name "mock-*.ts" -not -path "*/node_modules/*"
|
|
```
|
|
|
|
Should be empty.
|
|
|
|
- [ ] **Verify no `payload-` prefixed files exist**
|
|
|
|
```bash
|
|
find packages -name "payload-*.ts" -not -path "*/node_modules/*"
|
|
```
|
|
|
|
Should be empty.
|
|
|
|
- [ ] **Verify every use case has an `I*UseCase` type alias**
|
|
|
|
```bash
|
|
grep -L "export type I.*UseCase = ReturnType<typeof" packages/*/src/application/use-cases/*.ts
|
|
```
|
|
|
|
Should be empty (each file has the alias).
|
|
|
|
- [ ] **Verify every controller has an `I*Controller` type alias**
|
|
|
|
```bash
|
|
grep -L "export type I.*Controller = ReturnType<typeof" packages/*/src/interface-adapters/controllers/*.ts
|
|
```
|
|
|
|
Should be empty.
|
|
|
|
- [ ] **Update changelog with final summary**
|
|
|
|
Under each section, ensure full coverage. Add a "Summary" section at the top with totals: files added/modified/deleted/renamed.
|
|
|
|
- [ ] **Commit final cleanup**
|
|
|
|
```bash
|
|
git add packages/ docs/
|
|
git commit -m "chore(refactor-log): complete Lazar conformance changelog
|
|
|
|
Final summary entry plus any straggler imports caught during the
|
|
verification sweep. All features now structurally conformant per spec
|
|
§5; doc-update checklist ready for follow-up pass."
|
|
```
|
|
|
|
---
|
|
|
|
## Self-review (after writing the plan)
|
|
|
|
- [x] Spec coverage: all 6 features covered (auth, blog, marketing-pages, navigation, media + foundation tasks for entity split + file rename)
|
|
- [x] No placeholders — concrete code provided for every step
|
|
- [x] Type aliases (`I*UseCase`, `I*Controller`) defined in canonical examples
|
|
- [x] DI binding code shown explicitly with `.toDynamicValue()`
|
|
- [x] Test refactor pattern shown with direct injection
|
|
- [x] Refactor changelog scaffolded in Task 1 + populated incrementally
|
|
- [x] Each task is independently committable
|
|
- [x] Verification step at end of each task
|
|
|
|
## Execution
|
|
|
|
Per `superpowers:subagent-driven-development`: dispatch one implementer subagent per task with full task text + context, then spec compliance review, then code quality review, then proceed.
|