@plazmodium/odin 0.3.2-beta → 0.3.4-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +82 -11
  2. package/builtin/ODIN.md +1045 -0
  3. package/builtin/agent-definitions/README.md +170 -0
  4. package/builtin/agent-definitions/_shared-context.md +377 -0
  5. package/builtin/agent-definitions/architect.md +627 -0
  6. package/builtin/agent-definitions/builder.md +716 -0
  7. package/builtin/agent-definitions/discovery.md +293 -0
  8. package/builtin/agent-definitions/documenter.md +238 -0
  9. package/builtin/agent-definitions/guardian.md +1049 -0
  10. package/builtin/agent-definitions/integrator.md +363 -0
  11. package/builtin/agent-definitions/planning.md +236 -0
  12. package/builtin/agent-definitions/product.md +405 -0
  13. package/builtin/agent-definitions/release.md +430 -0
  14. package/builtin/agent-definitions/reviewer.md +447 -0
  15. package/builtin/agent-definitions/watcher.md +402 -0
  16. package/builtin/skills/api/graphql/SKILL.md +548 -0
  17. package/builtin/skills/api/grpc/SKILL.md +554 -0
  18. package/builtin/skills/api/rest-api/SKILL.md +469 -0
  19. package/builtin/skills/api/trpc/SKILL.md +503 -0
  20. package/builtin/skills/architecture/clean-architecture/SKILL.md +141 -0
  21. package/builtin/skills/architecture/domain-driven-design/SKILL.md +129 -0
  22. package/builtin/skills/architecture/event-driven/SKILL.md +145 -0
  23. package/builtin/skills/architecture/microservices/SKILL.md +143 -0
  24. package/builtin/skills/architecture/tla-precheck/SKILL.md +171 -0
  25. package/builtin/skills/backend/golang-gin/SKILL.md +141 -0
  26. package/builtin/skills/backend/nodejs-express/SKILL.md +277 -0
  27. package/builtin/skills/backend/nodejs-fastify/SKILL.md +152 -0
  28. package/builtin/skills/backend/python-django/SKILL.md +128 -0
  29. package/builtin/skills/backend/python-fastapi/SKILL.md +140 -0
  30. package/builtin/skills/database/mongodb/SKILL.md +132 -0
  31. package/builtin/skills/database/postgresql/SKILL.md +120 -0
  32. package/builtin/skills/database/prisma-orm/SKILL.md +366 -0
  33. package/builtin/skills/database/redis/SKILL.md +140 -0
  34. package/builtin/skills/database/supabase/SKILL.md +416 -0
  35. package/builtin/skills/devops/aws/SKILL.md +382 -0
  36. package/builtin/skills/devops/docker/SKILL.md +359 -0
  37. package/builtin/skills/devops/github-actions/SKILL.md +435 -0
  38. package/builtin/skills/devops/kubernetes/SKILL.md +459 -0
  39. package/builtin/skills/devops/terraform/SKILL.md +453 -0
  40. package/builtin/skills/frontend/alpine-dev/SKILL.md +27 -0
  41. package/builtin/skills/frontend/angular-dev/SKILL.md +28 -0
  42. package/builtin/skills/frontend/astro-dev/SKILL.md +28 -0
  43. package/builtin/skills/frontend/htmx-dev/SKILL.md +28 -0
  44. package/builtin/skills/frontend/nextjs-dev/SKILL.md +470 -0
  45. package/builtin/skills/frontend/react-patterns/SKILL.md +166 -0
  46. package/builtin/skills/frontend/svelte-dev/SKILL.md +28 -0
  47. package/builtin/skills/frontend/tailwindcss/SKILL.md +131 -0
  48. package/builtin/skills/frontend/vuejs-dev/SKILL.md +28 -0
  49. package/builtin/skills/generic-dev/SKILL.md +307 -0
  50. package/builtin/skills/testing/cypress/SKILL.md +372 -0
  51. package/builtin/skills/testing/jest/SKILL.md +176 -0
  52. package/builtin/skills/testing/playwright/SKILL.md +341 -0
  53. package/builtin/skills/testing/unit-tests-eval-sdd/SKILL.md +73 -0
  54. package/builtin/skills/testing/unit-tests-sdd/SKILL.md +83 -0
  55. package/builtin/skills/testing/vitest/SKILL.md +249 -0
  56. package/dist/adapters/skills/filesystem.d.ts.map +1 -1
  57. package/dist/adapters/skills/filesystem.js +2 -18
  58. package/dist/adapters/skills/filesystem.js.map +1 -1
  59. package/dist/builtin-assets.d.ts +8 -0
  60. package/dist/builtin-assets.d.ts.map +1 -0
  61. package/dist/builtin-assets.js +90 -0
  62. package/dist/builtin-assets.js.map +1 -0
  63. package/dist/init.js +69 -11
  64. package/dist/init.js.map +1 -1
  65. package/dist/schemas.d.ts +1 -1
  66. package/dist/server.js +1 -1
  67. package/dist/server.js.map +1 -1
  68. package/dist/tools/prepare-phase-context.d.ts.map +1 -1
  69. package/dist/tools/prepare-phase-context.js +5 -0
  70. package/dist/tools/prepare-phase-context.js.map +1 -1
  71. package/dist/types.d.ts +3 -0
  72. package/dist/types.d.ts.map +1 -1
  73. package/package.json +5 -3
@@ -0,0 +1,503 @@
1
+ ---
2
+ name: trpc
3
+ description: tRPC expertise for building end-to-end typesafe APIs. Covers routers, procedures, context, middleware, and React Query integration for full-stack TypeScript applications.
4
+ category: api
5
+ compatible_with:
6
+ - nextjs-dev
7
+ - react-patterns
8
+ - prisma-orm
9
+ ---
10
+
11
+ # tRPC End-to-End Typesafe APIs
12
+
13
+ ## Instructions
14
+
15
+ 1. **Assess the project**: tRPC is ideal for full-stack TypeScript monorepos.
16
+ 2. **Follow tRPC conventions**:
17
+ - Define routers and procedures
18
+ - Use Zod for input validation
19
+ - Leverage TypeScript inference
20
+ - Integrate with React Query
21
+ 3. **Provide complete examples**: Include server routers and client usage.
22
+ 4. **Guide on best practices**: Error handling, middleware, subscriptions.
23
+
24
+ ## Server Setup
25
+
26
+ ### Initialize tRPC
27
+
28
+ ```typescript
29
+ // server/trpc.ts
30
+ import { initTRPC, TRPCError } from '@trpc/server';
31
+ import { type Context } from './context';
32
+ import superjson from 'superjson';
33
+
34
+ const t = initTRPC.context<Context>().create({
35
+ transformer: superjson,
36
+ errorFormatter({ shape, error }) {
37
+ return {
38
+ ...shape,
39
+ data: {
40
+ ...shape.data,
41
+ zodError: error.cause instanceof ZodError
42
+ ? error.cause.flatten()
43
+ : null,
44
+ },
45
+ };
46
+ },
47
+ });
48
+
49
+ export const router = t.router;
50
+ export const publicProcedure = t.procedure;
51
+ export const middleware = t.middleware;
52
+ ```
53
+
54
+ ### Context
55
+
56
+ ```typescript
57
+ // server/context.ts
58
+ import { type inferAsyncReturnType } from '@trpc/server';
59
+ import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
60
+ import { getSession } from 'next-auth/react';
61
+ import { prisma } from './db';
62
+
63
+ export const createContext = async (opts: CreateNextContextOptions) => {
64
+ const session = await getSession({ req: opts.req });
65
+
66
+ return {
67
+ session,
68
+ user: session?.user,
69
+ prisma,
70
+ };
71
+ };
72
+
73
+ export type Context = inferAsyncReturnType<typeof createContext>;
74
+ ```
75
+
76
+ ### Middleware
77
+
78
+ ```typescript
79
+ // server/trpc.ts
80
+ const isAuthed = middleware(async ({ ctx, next }) => {
81
+ if (!ctx.session?.user) {
82
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
83
+ }
84
+ return next({
85
+ ctx: {
86
+ ...ctx,
87
+ user: ctx.session.user,
88
+ },
89
+ });
90
+ });
91
+
92
+ const isAdmin = middleware(async ({ ctx, next }) => {
93
+ if (ctx.user?.role !== 'ADMIN') {
94
+ throw new TRPCError({ code: 'FORBIDDEN' });
95
+ }
96
+ return next({ ctx });
97
+ });
98
+
99
+ export const protectedProcedure = t.procedure.use(isAuthed);
100
+ export const adminProcedure = t.procedure.use(isAuthed).use(isAdmin);
101
+ ```
102
+
103
+ ## Routers
104
+
105
+ ### Basic Router
106
+
107
+ ```typescript
108
+ // server/routers/user.ts
109
+ import { z } from 'zod';
110
+ import { router, publicProcedure, protectedProcedure } from '../trpc';
111
+ import { TRPCError } from '@trpc/server';
112
+
113
+ export const userRouter = router({
114
+ // Public query
115
+ getById: publicProcedure
116
+ .input(z.object({ id: z.string() }))
117
+ .query(async ({ ctx, input }) => {
118
+ const user = await ctx.prisma.user.findUnique({
119
+ where: { id: input.id },
120
+ select: { id: true, name: true, email: true, image: true },
121
+ });
122
+
123
+ if (!user) {
124
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
125
+ }
126
+
127
+ return user;
128
+ }),
129
+
130
+ // Protected query
131
+ me: protectedProcedure.query(async ({ ctx }) => {
132
+ return ctx.prisma.user.findUnique({
133
+ where: { id: ctx.user.id },
134
+ });
135
+ }),
136
+
137
+ // Mutation with input validation
138
+ update: protectedProcedure
139
+ .input(z.object({
140
+ name: z.string().min(1).max(100).optional(),
141
+ bio: z.string().max(500).optional(),
142
+ }))
143
+ .mutation(async ({ ctx, input }) => {
144
+ return ctx.prisma.user.update({
145
+ where: { id: ctx.user.id },
146
+ data: input,
147
+ });
148
+ }),
149
+ });
150
+ ```
151
+
152
+ ### Nested Router with Pagination
153
+
154
+ ```typescript
155
+ // server/routers/post.ts
156
+ import { z } from 'zod';
157
+ import { router, publicProcedure, protectedProcedure } from '../trpc';
158
+
159
+ export const postRouter = router({
160
+ list: publicProcedure
161
+ .input(z.object({
162
+ limit: z.number().min(1).max(100).default(20),
163
+ cursor: z.string().nullish(),
164
+ filter: z.object({
165
+ published: z.boolean().optional(),
166
+ authorId: z.string().optional(),
167
+ }).optional(),
168
+ }))
169
+ .query(async ({ ctx, input }) => {
170
+ const { limit, cursor, filter } = input;
171
+
172
+ const posts = await ctx.prisma.post.findMany({
173
+ take: limit + 1,
174
+ cursor: cursor ? { id: cursor } : undefined,
175
+ where: {
176
+ published: filter?.published,
177
+ authorId: filter?.authorId,
178
+ },
179
+ orderBy: { createdAt: 'desc' },
180
+ include: {
181
+ author: { select: { id: true, name: true, image: true } },
182
+ },
183
+ });
184
+
185
+ let nextCursor: string | undefined = undefined;
186
+ if (posts.length > limit) {
187
+ const nextItem = posts.pop();
188
+ nextCursor = nextItem!.id;
189
+ }
190
+
191
+ return {
192
+ items: posts,
193
+ nextCursor,
194
+ };
195
+ }),
196
+
197
+ create: protectedProcedure
198
+ .input(z.object({
199
+ title: z.string().min(1).max(200),
200
+ content: z.string().min(1),
201
+ published: z.boolean().default(false),
202
+ }))
203
+ .mutation(async ({ ctx, input }) => {
204
+ return ctx.prisma.post.create({
205
+ data: {
206
+ ...input,
207
+ authorId: ctx.user.id,
208
+ },
209
+ });
210
+ }),
211
+
212
+ update: protectedProcedure
213
+ .input(z.object({
214
+ id: z.string(),
215
+ title: z.string().min(1).max(200).optional(),
216
+ content: z.string().min(1).optional(),
217
+ published: z.boolean().optional(),
218
+ }))
219
+ .mutation(async ({ ctx, input }) => {
220
+ const { id, ...data } = input;
221
+
222
+ // Verify ownership
223
+ const post = await ctx.prisma.post.findUnique({ where: { id } });
224
+ if (!post || post.authorId !== ctx.user.id) {
225
+ throw new TRPCError({ code: 'FORBIDDEN' });
226
+ }
227
+
228
+ return ctx.prisma.post.update({
229
+ where: { id },
230
+ data,
231
+ });
232
+ }),
233
+
234
+ delete: protectedProcedure
235
+ .input(z.object({ id: z.string() }))
236
+ .mutation(async ({ ctx, input }) => {
237
+ const post = await ctx.prisma.post.findUnique({
238
+ where: { id: input.id }
239
+ });
240
+
241
+ if (!post || post.authorId !== ctx.user.id) {
242
+ throw new TRPCError({ code: 'FORBIDDEN' });
243
+ }
244
+
245
+ await ctx.prisma.post.delete({ where: { id: input.id } });
246
+ return { success: true };
247
+ }),
248
+ });
249
+ ```
250
+
251
+ ### Root Router
252
+
253
+ ```typescript
254
+ // server/routers/_app.ts
255
+ import { router } from '../trpc';
256
+ import { userRouter } from './user';
257
+ import { postRouter } from './post';
258
+
259
+ export const appRouter = router({
260
+ user: userRouter,
261
+ post: postRouter,
262
+ });
263
+
264
+ export type AppRouter = typeof appRouter;
265
+ ```
266
+
267
+ ## Client Setup
268
+
269
+ ### React Client
270
+
271
+ ```typescript
272
+ // utils/trpc.ts
273
+ import { createTRPCReact } from '@trpc/react-query';
274
+ import { type AppRouter } from '../server/routers/_app';
275
+
276
+ export const trpc = createTRPCReact<AppRouter>();
277
+ ```
278
+
279
+ ### Provider Setup
280
+
281
+ ```typescript
282
+ // pages/_app.tsx or app/providers.tsx
283
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
284
+ import { httpBatchLink } from '@trpc/client';
285
+ import { trpc } from '../utils/trpc';
286
+ import superjson from 'superjson';
287
+
288
+ const queryClient = new QueryClient({
289
+ defaultOptions: {
290
+ queries: {
291
+ staleTime: 5 * 60 * 1000,
292
+ refetchOnWindowFocus: false,
293
+ },
294
+ },
295
+ });
296
+
297
+ const trpcClient = trpc.createClient({
298
+ links: [
299
+ httpBatchLink({
300
+ url: '/api/trpc',
301
+ transformer: superjson,
302
+ headers() {
303
+ return {
304
+ // Add auth headers if needed
305
+ };
306
+ },
307
+ }),
308
+ ],
309
+ });
310
+
311
+ export function Providers({ children }: { children: React.ReactNode }) {
312
+ return (
313
+ <trpc.Provider client={trpcClient} queryClient={queryClient}>
314
+ <QueryClientProvider client={queryClient}>
315
+ {children}
316
+ </QueryClientProvider>
317
+ </trpc.Provider>
318
+ );
319
+ }
320
+ ```
321
+
322
+ ## Client Usage
323
+
324
+ ### Queries
325
+
326
+ ```typescript
327
+ function UserProfile({ userId }: { userId: string }) {
328
+ const { data, isLoading, error } = trpc.user.getById.useQuery({ id: userId });
329
+
330
+ if (isLoading) return <Spinner />;
331
+ if (error) return <Error message={error.message} />;
332
+
333
+ return (
334
+ <div>
335
+ <h1>{data.name}</h1>
336
+ <p>{data.email}</p>
337
+ </div>
338
+ );
339
+ }
340
+
341
+ // With options
342
+ const { data } = trpc.post.list.useQuery(
343
+ { limit: 10, filter: { published: true } },
344
+ {
345
+ enabled: isLoggedIn,
346
+ refetchInterval: 30000,
347
+ }
348
+ );
349
+ ```
350
+
351
+ ### Infinite Queries
352
+
353
+ ```typescript
354
+ function PostFeed() {
355
+ const {
356
+ data,
357
+ fetchNextPage,
358
+ hasNextPage,
359
+ isFetchingNextPage,
360
+ } = trpc.post.list.useInfiniteQuery(
361
+ { limit: 20 },
362
+ {
363
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
364
+ }
365
+ );
366
+
367
+ return (
368
+ <>
369
+ {data?.pages.map((page) =>
370
+ page.items.map((post) => <PostCard key={post.id} post={post} />)
371
+ )}
372
+
373
+ {hasNextPage && (
374
+ <button
375
+ onClick={() => fetchNextPage()}
376
+ disabled={isFetchingNextPage}
377
+ >
378
+ {isFetchingNextPage ? 'Loading...' : 'Load More'}
379
+ </button>
380
+ )}
381
+ </>
382
+ );
383
+ }
384
+ ```
385
+
386
+ ### Mutations
387
+
388
+ ```typescript
389
+ function CreatePostForm() {
390
+ const utils = trpc.useUtils();
391
+
392
+ const createPost = trpc.post.create.useMutation({
393
+ onSuccess: () => {
394
+ // Invalidate and refetch
395
+ utils.post.list.invalidate();
396
+ },
397
+ onError: (error) => {
398
+ if (error.data?.zodError) {
399
+ // Handle validation errors
400
+ console.log(error.data.zodError.fieldErrors);
401
+ }
402
+ },
403
+ });
404
+
405
+ const handleSubmit = (values: { title: string; content: string }) => {
406
+ createPost.mutate(values);
407
+ };
408
+
409
+ return (
410
+ <form onSubmit={handleSubmit}>
411
+ {/* Form fields */}
412
+ <button type="submit" disabled={createPost.isPending}>
413
+ {createPost.isPending ? 'Creating...' : 'Create Post'}
414
+ </button>
415
+ </form>
416
+ );
417
+ }
418
+ ```
419
+
420
+ ### Optimistic Updates
421
+
422
+ ```typescript
423
+ const updatePost = trpc.post.update.useMutation({
424
+ onMutate: async (newData) => {
425
+ // Cancel outgoing refetches
426
+ await utils.post.getById.cancel({ id: newData.id });
427
+
428
+ // Snapshot previous value
429
+ const previousPost = utils.post.getById.getData({ id: newData.id });
430
+
431
+ // Optimistically update
432
+ utils.post.getById.setData({ id: newData.id }, (old) => ({
433
+ ...old!,
434
+ ...newData,
435
+ }));
436
+
437
+ return { previousPost };
438
+ },
439
+ onError: (err, newData, context) => {
440
+ // Rollback on error
441
+ utils.post.getById.setData(
442
+ { id: newData.id },
443
+ context?.previousPost
444
+ );
445
+ },
446
+ onSettled: (data, error, variables) => {
447
+ // Refetch after mutation
448
+ utils.post.getById.invalidate({ id: variables.id });
449
+ },
450
+ });
451
+ ```
452
+
453
+ ## Subscriptions (WebSocket)
454
+
455
+ ### Server
456
+
457
+ ```typescript
458
+ import { observable } from '@trpc/server/observable';
459
+
460
+ export const notificationRouter = router({
461
+ onNew: protectedProcedure.subscription(({ ctx }) => {
462
+ return observable<Notification>((emit) => {
463
+ const onNotification = (notification: Notification) => {
464
+ if (notification.userId === ctx.user.id) {
465
+ emit.next(notification);
466
+ }
467
+ };
468
+
469
+ eventEmitter.on('notification', onNotification);
470
+
471
+ return () => {
472
+ eventEmitter.off('notification', onNotification);
473
+ };
474
+ });
475
+ }),
476
+ });
477
+ ```
478
+
479
+ ### Client
480
+
481
+ ```typescript
482
+ trpc.notification.onNew.useSubscription(undefined, {
483
+ onData: (notification) => {
484
+ toast.info(notification.message);
485
+ },
486
+ });
487
+ ```
488
+
489
+ ## Best Practices
490
+
491
+ - **Use Zod schemas** - Single source of truth for validation
492
+ - **Leverage inference** - Let TypeScript infer types from schemas
493
+ - **Batch requests** - Use `httpBatchLink` for multiple queries
494
+ - **Invalidate wisely** - Only invalidate what's necessary
495
+ - **Error boundaries** - Wrap components that use queries
496
+ - **Prefetch data** - Use `prefetch` for anticipated navigation
497
+ - **Keep routers focused** - One domain per router file
498
+
499
+ ## References
500
+
501
+ - tRPC Documentation: https://trpc.io/docs
502
+ - tRPC + Next.js: https://trpc.io/docs/nextjs
503
+ - tRPC + React Query: https://trpc.io/docs/react-query
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: clean-architecture
3
+ description: Clean Architecture principles — dependency inversion, use cases, and layer separation
4
+ category: architecture
5
+ version: "1.0"
6
+ compatible_with:
7
+ - nodejs-express
8
+ - nodejs-fastify
9
+ - python-fastapi
10
+ - python-django
11
+ - golang-gin
12
+ ---
13
+
14
+ # Clean Architecture
15
+
16
+ ## Overview
17
+
18
+ Clean Architecture separates code into concentric layers with the Dependency Rule: source code dependencies always point inward. Business logic never depends on frameworks, databases, or UI.
19
+
20
+ ## Layer Structure
21
+
22
+ ```
23
+ src/
24
+ ├── domain/ # Innermost — entities + business rules
25
+ │ ├── entities/
26
+ │ │ └── User.ts
27
+ │ └── value-objects/
28
+ │ └── Email.ts
29
+ ├── application/ # Use cases — orchestrate domain logic
30
+ │ ├── use-cases/
31
+ │ │ ├── CreateUser.ts
32
+ │ │ └── GetUserById.ts
33
+ │ └── ports/ # Interfaces (driven/driving)
34
+ │ ├── UserRepository.ts
35
+ │ └── EmailService.ts
36
+ ├── infrastructure/ # Outermost — frameworks, DB, external APIs
37
+ │ ├── persistence/
38
+ │ │ └── PgUserRepository.ts
39
+ │ ├── services/
40
+ │ │ └── SendGridEmailService.ts
41
+ │ └── web/
42
+ │ ├── routes.ts
43
+ │ └── controllers/
44
+ │ └── UserController.ts
45
+ └── main.ts # Composition root — wires everything together
46
+ ```
47
+
48
+ ## Core Patterns
49
+
50
+ ### Entity (Domain Layer)
51
+
52
+ ```typescript
53
+ // Pure business logic — no framework imports
54
+ export class User {
55
+ constructor(
56
+ public readonly id: string,
57
+ public readonly email: Email,
58
+ public name: string,
59
+ private passwordHash: string,
60
+ ) {}
61
+
62
+ changeName(newName: string): void {
63
+ if (newName.length < 2) throw new DomainError('Name too short');
64
+ this.name = newName;
65
+ }
66
+
67
+ verifyPassword(plaintext: string): boolean {
68
+ return hashCompare(plaintext, this.passwordHash);
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### Use Case (Application Layer)
74
+
75
+ ```typescript
76
+ // Depends only on ports (interfaces), not implementations
77
+ export class CreateUser {
78
+ constructor(
79
+ private userRepo: UserRepository,
80
+ private emailService: EmailService,
81
+ ) {}
82
+
83
+ async execute(input: { email: string; name: string; password: string }): Promise<User> {
84
+ const existing = await this.userRepo.findByEmail(input.email);
85
+ if (existing) throw new ConflictError('Email already registered');
86
+
87
+ const user = User.create(input);
88
+ await this.userRepo.save(user);
89
+ await this.emailService.sendWelcome(user.email);
90
+ return user;
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Port (Interface)
96
+
97
+ ```typescript
98
+ // Defined in application layer — implemented in infrastructure
99
+ export interface UserRepository {
100
+ findById(id: string): Promise<User | null>;
101
+ findByEmail(email: string): Promise<User | null>;
102
+ save(user: User): Promise<void>;
103
+ }
104
+ ```
105
+
106
+ ### Adapter (Infrastructure Layer)
107
+
108
+ ```typescript
109
+ // Implements the port — depends on the interface, not the other way
110
+ export class PgUserRepository implements UserRepository {
111
+ constructor(private db: Pool) {}
112
+
113
+ async findById(id: string): Promise<User | null> {
114
+ const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
115
+ return row.rows[0] ? this.toDomain(row.rows[0]) : null;
116
+ }
117
+
118
+ async save(user: User): Promise<void> {
119
+ await this.db.query(
120
+ 'INSERT INTO users (id, email, name, password_hash) VALUES ($1, $2, $3, $4)',
121
+ [user.id, user.email.value, user.name, user.passwordHash],
122
+ );
123
+ }
124
+ }
125
+ ```
126
+
127
+ ## Best Practices
128
+
129
+ 1. **Dependency Rule** — inner layers never import from outer layers
130
+ 2. **Composition Root** — wire dependencies in `main.ts`, not inside classes
131
+ 3. **Use Case = 1 operation** — each use case does one thing
132
+ 4. **Domain has no dependencies** — no ORM decorators, no framework types
133
+ 5. **Test use cases** with mock ports — fast, no DB needed
134
+ 6. **Value Objects** for validated primitives (Email, Money, UserId)
135
+
136
+ ## Gotchas
137
+
138
+ - **Over-engineering** — don't add layers for trivial CRUD; use clean architecture for complex domains
139
+ - **Mapping fatigue** — converting between layers (DB row → domain entity → DTO) adds boilerplate
140
+ - **Framework leakage** — ORM decorators on domain entities break the dependency rule
141
+ - **Where does validation go?** — input validation at the boundary, business rules in the domain