red64-cli 0.1.0 → 0.3.0

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 (125) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/parseArgs.d.ts.map +1 -1
  3. package/dist/cli/parseArgs.js +5 -0
  4. package/dist/cli/parseArgs.js.map +1 -1
  5. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  6. package/dist/components/init/CompleteStep.js +2 -2
  7. package/dist/components/init/CompleteStep.js.map +1 -1
  8. package/dist/components/init/TestCheckStep.d.ts +16 -0
  9. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  10. package/dist/components/init/TestCheckStep.js +120 -0
  11. package/dist/components/init/TestCheckStep.js.map +1 -0
  12. package/dist/components/init/index.d.ts +1 -0
  13. package/dist/components/init/index.d.ts.map +1 -1
  14. package/dist/components/init/index.js +1 -0
  15. package/dist/components/init/index.js.map +1 -1
  16. package/dist/components/init/types.d.ts +9 -0
  17. package/dist/components/init/types.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  19. package/dist/components/screens/InitScreen.js +69 -6
  20. package/dist/components/screens/InitScreen.js.map +1 -1
  21. package/dist/components/screens/ListScreen.d.ts.map +1 -1
  22. package/dist/components/screens/ListScreen.js +28 -3
  23. package/dist/components/screens/ListScreen.js.map +1 -1
  24. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  25. package/dist/components/screens/StartScreen.js +212 -13
  26. package/dist/components/screens/StartScreen.js.map +1 -1
  27. package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
  28. package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
  29. package/dist/components/ui/ArtifactsSidebar.js +51 -0
  30. package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
  31. package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
  32. package/dist/components/ui/FeatureSidebar.js +1 -1
  33. package/dist/components/ui/FeatureSidebar.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +1 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +1 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/services/ClaudeErrorDetector.js +3 -3
  39. package/dist/services/ClaudeErrorDetector.js.map +1 -1
  40. package/dist/services/ConfigService.d.ts +1 -0
  41. package/dist/services/ConfigService.d.ts.map +1 -1
  42. package/dist/services/ConfigService.js.map +1 -1
  43. package/dist/services/ProjectDetector.d.ts +28 -0
  44. package/dist/services/ProjectDetector.d.ts.map +1 -0
  45. package/dist/services/ProjectDetector.js +236 -0
  46. package/dist/services/ProjectDetector.js.map +1 -0
  47. package/dist/services/TestRunner.d.ts +46 -0
  48. package/dist/services/TestRunner.d.ts.map +1 -0
  49. package/dist/services/TestRunner.js +85 -0
  50. package/dist/services/TestRunner.js.map +1 -0
  51. package/dist/services/index.d.ts +2 -0
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/index.js +2 -0
  54. package/dist/services/index.js.map +1 -1
  55. package/dist/types/index.d.ts +13 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/index.js.map +1 -1
  58. package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
  59. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  60. package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
  61. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  62. package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
  63. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  64. package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
  65. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  66. package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
  67. package/framework/stacks/generic/feedback.md +80 -0
  68. package/framework/stacks/nextjs/accessibility.md +437 -0
  69. package/framework/stacks/nextjs/api.md +431 -0
  70. package/framework/stacks/nextjs/coding-style.md +282 -0
  71. package/framework/stacks/nextjs/commenting.md +226 -0
  72. package/framework/stacks/nextjs/components.md +411 -0
  73. package/framework/stacks/nextjs/conventions.md +333 -0
  74. package/framework/stacks/nextjs/css.md +310 -0
  75. package/framework/stacks/nextjs/error-handling.md +442 -0
  76. package/framework/stacks/nextjs/feedback.md +124 -0
  77. package/framework/stacks/nextjs/migrations.md +332 -0
  78. package/framework/stacks/nextjs/models.md +362 -0
  79. package/framework/stacks/nextjs/queries.md +410 -0
  80. package/framework/stacks/nextjs/responsive.md +338 -0
  81. package/framework/stacks/nextjs/tech-stack.md +177 -0
  82. package/framework/stacks/nextjs/test-writing.md +475 -0
  83. package/framework/stacks/nextjs/validation.md +467 -0
  84. package/framework/stacks/python/api.md +468 -0
  85. package/framework/stacks/python/authentication.md +342 -0
  86. package/framework/stacks/python/code-quality.md +283 -0
  87. package/framework/stacks/python/code-refactoring.md +315 -0
  88. package/framework/stacks/python/coding-style.md +462 -0
  89. package/framework/stacks/python/conventions.md +399 -0
  90. package/framework/stacks/python/error-handling.md +512 -0
  91. package/framework/stacks/python/feedback.md +92 -0
  92. package/framework/stacks/python/implement-ai-llm.md +468 -0
  93. package/framework/stacks/python/migrations.md +388 -0
  94. package/framework/stacks/python/models.md +399 -0
  95. package/framework/stacks/python/python.md +232 -0
  96. package/framework/stacks/python/queries.md +451 -0
  97. package/framework/stacks/python/structure.md +245 -58
  98. package/framework/stacks/python/tech.md +92 -35
  99. package/framework/stacks/python/testing.md +380 -0
  100. package/framework/stacks/python/validation.md +471 -0
  101. package/framework/stacks/rails/authentication.md +176 -0
  102. package/framework/stacks/rails/code-quality.md +287 -0
  103. package/framework/stacks/rails/code-refactoring.md +299 -0
  104. package/framework/stacks/rails/feedback.md +130 -0
  105. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  106. package/framework/stacks/rails/rails.md +301 -0
  107. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  108. package/framework/stacks/rails/rails8-css.md +573 -0
  109. package/framework/stacks/rails/structure.md +140 -0
  110. package/framework/stacks/rails/tech.md +108 -0
  111. package/framework/stacks/react/code-quality.md +521 -0
  112. package/framework/stacks/react/components.md +625 -0
  113. package/framework/stacks/react/data-fetching.md +586 -0
  114. package/framework/stacks/react/feedback.md +110 -0
  115. package/framework/stacks/react/forms.md +694 -0
  116. package/framework/stacks/react/performance.md +640 -0
  117. package/framework/stacks/react/product.md +22 -9
  118. package/framework/stacks/react/state-management.md +472 -0
  119. package/framework/stacks/react/structure.md +351 -44
  120. package/framework/stacks/react/tech.md +219 -30
  121. package/framework/stacks/react/testing.md +690 -0
  122. package/package.json +1 -1
  123. package/framework/stacks/node/product.md +0 -27
  124. package/framework/stacks/node/structure.md +0 -82
  125. package/framework/stacks/node/tech.md +0 -63
@@ -0,0 +1,399 @@
1
+ # SQLAlchemy Model Patterns
2
+
3
+ Best practices for SQLAlchemy 2.0 declarative models in modern Python projects.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ - **Database enforces integrity**: Constraints live in the schema, not just application code
10
+ - **Models are thin**: Business logic belongs in services, not models
11
+ - **Explicit relationships**: Define loading strategy at query time, not model time
12
+ - **Type-safe by default**: Use `Mapped[]` annotations for all columns
13
+
14
+ ---
15
+
16
+ ## Base Model
17
+
18
+ ### Declarative Base with Conventions
19
+
20
+ ```python
21
+ # app/models/base.py
22
+ from datetime import datetime
23
+ from typing import Any
24
+
25
+ from sqlalchemy import MetaData, func
26
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
27
+
28
+ # Naming conventions for constraints (required for Alembic)
29
+ convention = {
30
+ "ix": "ix_%(column_0_label)s",
31
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
32
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
33
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
34
+ "pk": "pk_%(table_name)s",
35
+ }
36
+
37
+ class Base(DeclarativeBase):
38
+ metadata = MetaData(naming_convention=convention)
39
+ ```
40
+
41
+ **Why naming conventions**: Alembic needs predictable constraint names to generate reliable downgrade migrations. Without them, auto-generated names differ across databases.
42
+
43
+ ---
44
+
45
+ ## Timestamp Mixin
46
+
47
+ ### Always Include Timestamps
48
+
49
+ ```python
50
+ # app/models/mixins.py
51
+ from datetime import datetime
52
+ from sqlalchemy import func
53
+ from sqlalchemy.orm import Mapped, mapped_column
54
+
55
+ class TimestampMixin:
56
+ """Add created_at and updated_at to any model."""
57
+
58
+ created_at: Mapped[datetime] = mapped_column(
59
+ server_default=func.now(),
60
+ nullable=False,
61
+ )
62
+ updated_at: Mapped[datetime] = mapped_column(
63
+ server_default=func.now(),
64
+ onupdate=func.now(),
65
+ nullable=False,
66
+ )
67
+ ```
68
+
69
+ **Usage**: Every table gets timestamps. No exceptions.
70
+
71
+ ```python
72
+ class User(TimestampMixin, Base):
73
+ __tablename__ = "users"
74
+ ...
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Model Definition (SQLAlchemy 2.0 Style)
80
+
81
+ ### Complete Example
82
+
83
+ ```python
84
+ # app/models/user.py
85
+ from __future__ import annotations
86
+
87
+ from sqlalchemy import String, Text
88
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
89
+
90
+ from app.models.base import Base
91
+ from app.models.mixins import TimestampMixin, SoftDeleteMixin
92
+
93
+
94
+ class User(TimestampMixin, SoftDeleteMixin, Base):
95
+ __tablename__ = "users"
96
+
97
+ id: Mapped[int] = mapped_column(primary_key=True)
98
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
99
+ name: Mapped[str] = mapped_column(String(255))
100
+ hashed_password: Mapped[str] = mapped_column(String(255))
101
+ bio: Mapped[str | None] = mapped_column(Text, default=None)
102
+ is_active: Mapped[bool] = mapped_column(default=True)
103
+ role: Mapped[str] = mapped_column(String(20), default="member")
104
+
105
+ # Relationships
106
+ posts: Mapped[list[Post]] = relationship(back_populates="author", cascade="all, delete-orphan")
107
+
108
+ def __repr__(self) -> str:
109
+ return f"<User id={self.id} email={self.email!r}>"
110
+ ```
111
+
112
+ ### Key Conventions
113
+
114
+ | Convention | Example | Reason |
115
+ |---|---|---|
116
+ | Singular model name | `User`, not `Users` | Python class convention |
117
+ | Plural table name | `"users"` | SQL convention |
118
+ | `Mapped[type]` for all columns | `Mapped[str]` | Type safety, IDE support |
119
+ | `Mapped[T \| None]` for nullable | `Mapped[str \| None]` | Explicit nullability |
120
+ | String lengths on VARCHAR | `String(255)` | Prevent unbounded columns |
121
+ | Index on foreign keys | `index=True` | Query performance |
122
+
123
+ ---
124
+
125
+ ## Data Integrity
126
+
127
+ ### Constraints
128
+
129
+ ```python
130
+ from sqlalchemy import CheckConstraint, UniqueConstraint, Index
131
+
132
+ class Order(TimestampMixin, Base):
133
+ __tablename__ = "orders"
134
+ __table_args__ = (
135
+ UniqueConstraint("user_id", "external_id", name="uq_orders_user_external"),
136
+ CheckConstraint("total_cents >= 0", name="positive_total"),
137
+ Index("ix_orders_user_status", "user_id", "status"),
138
+ )
139
+
140
+ id: Mapped[int] = mapped_column(primary_key=True)
141
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
142
+ external_id: Mapped[str] = mapped_column(String(100))
143
+ status: Mapped[str] = mapped_column(String(20), default="pending")
144
+ total_cents: Mapped[int] = mapped_column()
145
+ ```
146
+
147
+ ### When to Use Each Constraint
148
+
149
+ | Constraint | Use Case |
150
+ |---|---|
151
+ | `unique=True` | Single-column uniqueness (email, slug) |
152
+ | `UniqueConstraint` | Multi-column uniqueness |
153
+ | `CheckConstraint` | Value range, format rules |
154
+ | `ForeignKey` | Referential integrity |
155
+ | `Index` | Composite indexes, partial indexes |
156
+ | `nullable=False` (default for `Mapped[T]`) | Required fields |
157
+
158
+ ---
159
+
160
+ ## Relationships
161
+
162
+ ### Loading Strategies
163
+
164
+ ```python
165
+ class Post(TimestampMixin, Base):
166
+ __tablename__ = "posts"
167
+
168
+ id: Mapped[int] = mapped_column(primary_key=True)
169
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
170
+ title: Mapped[str] = mapped_column(String(500))
171
+ body: Mapped[str] = mapped_column(Text)
172
+ status: Mapped[str] = mapped_column(String(20), default="draft")
173
+
174
+ # Define relationship, but do NOT set eager loading here
175
+ author: Mapped[User] = relationship(back_populates="posts")
176
+ tags: Mapped[list[Tag]] = relationship(secondary="post_tags", back_populates="posts")
177
+ comments: Mapped[list[Comment]] = relationship(back_populates="post", cascade="all, delete-orphan")
178
+ ```
179
+
180
+ **Rule**: Never set `lazy="joined"` or `lazy="selectin"` on the model. Choose loading strategy at query time:
181
+
182
+ ```python
183
+ # At query time, choose the right strategy
184
+ from sqlalchemy.orm import selectinload, joinedload
185
+
186
+ # For collections: selectinload (separate SELECT IN query)
187
+ stmt = select(Post).options(selectinload(Post.comments))
188
+
189
+ # For single relations: joinedload (JOIN in same query)
190
+ stmt = select(Post).options(joinedload(Post.author))
191
+
192
+ # Nested loading
193
+ stmt = select(User).options(
194
+ selectinload(User.posts).selectinload(Post.comments)
195
+ )
196
+ ```
197
+
198
+ ### Cascade Behaviors
199
+
200
+ ```python
201
+ # Delete orphans when parent is deleted
202
+ posts: Mapped[list[Post]] = relationship(cascade="all, delete-orphan")
203
+
204
+ # Nullify foreign key when parent is deleted
205
+ posts: Mapped[list[Post]] = relationship(passive_deletes=True)
206
+ # Requires: ForeignKey("users.id", ondelete="SET NULL")
207
+
208
+ # Database-level cascade (preferred for performance)
209
+ user_id: Mapped[int] = mapped_column(
210
+ ForeignKey("users.id", ondelete="CASCADE"),
211
+ index=True,
212
+ )
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Soft Deletes
218
+
219
+ ### Mixin Pattern
220
+
221
+ ```python
222
+ # app/models/mixins.py
223
+ from datetime import datetime
224
+ from sqlalchemy.orm import Mapped, mapped_column
225
+
226
+ class SoftDeleteMixin:
227
+ """Soft delete support. Query with .where(Model.deleted_at.is_(None))."""
228
+
229
+ deleted_at: Mapped[datetime | None] = mapped_column(default=None, index=True)
230
+
231
+ @property
232
+ def is_deleted(self) -> bool:
233
+ return self.deleted_at is not None
234
+ ```
235
+
236
+ ### Querying with Soft Deletes
237
+
238
+ ```python
239
+ # Active records only
240
+ stmt = select(User).where(User.deleted_at.is_(None))
241
+
242
+ # Include deleted
243
+ stmt = select(User)
244
+
245
+ # Deleted only
246
+ stmt = select(User).where(User.deleted_at.is_not(None))
247
+
248
+ # Soft delete operation
249
+ user.deleted_at = datetime.now(timezone.utc)
250
+ await db.commit()
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Hybrid Properties
256
+
257
+ ### Computed Values
258
+
259
+ ```python
260
+ from sqlalchemy.ext.hybrid import hybrid_property
261
+
262
+ class User(TimestampMixin, Base):
263
+ __tablename__ = "users"
264
+
265
+ first_name: Mapped[str] = mapped_column(String(100))
266
+ last_name: Mapped[str] = mapped_column(String(100))
267
+
268
+ @hybrid_property
269
+ def full_name(self) -> str:
270
+ return f"{self.first_name} {self.last_name}"
271
+
272
+ @full_name.expression
273
+ @classmethod
274
+ def full_name(cls):
275
+ return cls.first_name + " " + cls.last_name
276
+ ```
277
+
278
+ This allows both Python-side and SQL-side usage:
279
+
280
+ ```python
281
+ # Python
282
+ user.full_name # "John Doe"
283
+
284
+ # SQL
285
+ stmt = select(User).where(User.full_name == "John Doe")
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Pydantic Integration
291
+
292
+ ### Model to Schema
293
+
294
+ ```python
295
+ # app/schemas/user.py
296
+ from pydantic import BaseModel, ConfigDict
297
+
298
+ class UserResponse(BaseModel):
299
+ model_config = ConfigDict(from_attributes=True)
300
+
301
+ id: int
302
+ email: str
303
+ name: str
304
+ is_active: bool
305
+ created_at: datetime
306
+
307
+ class UserCreate(BaseModel):
308
+ email: str
309
+ name: str
310
+ password: str # Plain text, hashed before storage
311
+
312
+ class UserUpdate(BaseModel):
313
+ name: str | None = None
314
+ bio: str | None = None
315
+ ```
316
+
317
+ ### Usage
318
+
319
+ ```python
320
+ # SQLAlchemy model -> Pydantic schema
321
+ user = await db.get(User, user_id)
322
+ response = UserResponse.model_validate(user)
323
+
324
+ # Pydantic schema -> dict for creation
325
+ data = UserCreate(email="a@b.com", name="Test", password="secret")
326
+ user = User(**data.model_dump(exclude={"password"}), hashed_password=hash_password(data.password))
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Enums
332
+
333
+ ### Use Python StrEnum
334
+
335
+ ```python
336
+ from enum import StrEnum
337
+
338
+ class PostStatus(StrEnum):
339
+ DRAFT = "draft"
340
+ PUBLISHED = "published"
341
+ ARCHIVED = "archived"
342
+
343
+ class Post(TimestampMixin, Base):
344
+ __tablename__ = "posts"
345
+
346
+ # Store as string, not database ENUM (easier migrations)
347
+ status: Mapped[str] = mapped_column(String(20), default=PostStatus.DRAFT)
348
+ ```
349
+
350
+ **Why string over DB ENUM**: Adding values to a PostgreSQL ENUM requires a migration. String columns with application-level validation are simpler to evolve.
351
+
352
+ ---
353
+
354
+ ## Association Tables
355
+
356
+ ### Many-to-Many
357
+
358
+ ```python
359
+ from sqlalchemy import Table, Column, ForeignKey
360
+
361
+ post_tags = Table(
362
+ "post_tags",
363
+ Base.metadata,
364
+ Column("post_id", ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True),
365
+ Column("tag_id", ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True),
366
+ )
367
+ ```
368
+
369
+ ### Association Object (with extra data)
370
+
371
+ ```python
372
+ class PostTag(TimestampMixin, Base):
373
+ __tablename__ = "post_tags"
374
+
375
+ post_id: Mapped[int] = mapped_column(ForeignKey("posts.id"), primary_key=True)
376
+ tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
377
+ added_by: Mapped[int] = mapped_column(ForeignKey("users.id"))
378
+ position: Mapped[int] = mapped_column(default=0)
379
+
380
+ post: Mapped[Post] = relationship()
381
+ tag: Mapped[Tag] = relationship()
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Anti-Patterns
387
+
388
+ | Anti-Pattern | Problem | Correct Approach |
389
+ |---|---|---|
390
+ | Business logic in models | Models become hard to test | Put logic in service layer |
391
+ | `lazy="joined"` on model | Always loads relation, N+1 risk | Choose strategy at query time |
392
+ | No constraint naming convention | Alembic generates unstable names | Use `MetaData(naming_convention=...)` |
393
+ | Missing `__repr__` | Debugging is painful | Always define `__repr__` |
394
+ | `String()` without length | Unbounded columns, DB-specific behavior | Always specify `String(n)` |
395
+ | Importing models in migrations | Models change, migrations break | Use `sa.table()` in migrations |
396
+
397
+ ---
398
+
399
+ _Models define structure and integrity. Business rules belong in services. Loading strategies belong in queries._
@@ -0,0 +1,232 @@
1
+ # Python Conventions
2
+
3
+ Project memory for modern Python 3.12+ patterns and conventions.
4
+
5
+ ---
6
+
7
+ ## Language Stack
8
+
9
+ ### Core Technologies
10
+ - **Python 3.12+** with modern typing features
11
+ - **uv**: Fast package management and virtual environments
12
+ - **Pydantic v2**: Data validation and settings management
13
+ - **asyncio**: Native async/await for concurrent operations
14
+ - **structlog**: Structured logging
15
+
16
+ ---
17
+
18
+ ## Type System
19
+
20
+ ### Modern Typing (Python 3.12+)
21
+
22
+ Use built-in generics and the new `type` statement:
23
+
24
+ ```python
25
+ # Pattern: Modern type aliases (3.12+)
26
+ type UserId = int
27
+ type Result[T] = T | None
28
+ type Handler = Callable[[Request], Awaitable[Response]]
29
+
30
+ # Pattern: Built-in generics (3.10+), no imports needed
31
+ def get_items(ids: list[int]) -> dict[str, Item]:
32
+ ...
33
+
34
+ # Pattern: Union with pipe operator (3.10+)
35
+ def find_user(key: str | int) -> User | None:
36
+ ...
37
+ ```
38
+
39
+ ### Protocols Over ABCs
40
+
41
+ Prefer `Protocol` for structural subtyping:
42
+
43
+ ```python
44
+ from typing import Protocol, runtime_checkable
45
+
46
+ @runtime_checkable
47
+ class Repository(Protocol):
48
+ async def get(self, id: str) -> dict: ...
49
+ async def save(self, entity: dict) -> None: ...
50
+
51
+ # Any class with matching methods satisfies the protocol
52
+ class PostgresRepository:
53
+ async def get(self, id: str) -> dict:
54
+ ...
55
+ async def save(self, entity: dict) -> None:
56
+ ...
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Data Modeling
62
+
63
+ ### Pydantic for External Data
64
+
65
+ Use Pydantic models for API boundaries, config, and validation:
66
+
67
+ ```python
68
+ from pydantic import BaseModel, Field, field_validator
69
+
70
+ class CreateUserRequest(BaseModel):
71
+ email: str = Field(..., pattern=r"^[\w.+-]+@[\w-]+\.[\w.]+$")
72
+ name: str = Field(..., min_length=1, max_length=100)
73
+ role: str = "member"
74
+
75
+ @field_validator("role")
76
+ @classmethod
77
+ def validate_role(cls, v: str) -> str:
78
+ if v not in ("admin", "member", "viewer"):
79
+ raise ValueError(f"Invalid role: {v}")
80
+ return v
81
+ ```
82
+
83
+ ### Dataclasses for Internal Data
84
+
85
+ Use `dataclass` for simple internal value objects:
86
+
87
+ ```python
88
+ from dataclasses import dataclass, field
89
+ from datetime import datetime
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class AuditEntry:
93
+ action: str
94
+ user_id: str
95
+ timestamp: datetime = field(default_factory=datetime.utcnow)
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Async Patterns
101
+
102
+ ### Async Service Pattern
103
+
104
+ ```python
105
+ import asyncio
106
+
107
+ class ContentService:
108
+ def __init__(self, repo: Repository, cache: CacheClient) -> None:
109
+ self._repo = repo
110
+ self._cache = cache
111
+
112
+ async def get_content(self, content_id: str) -> Content:
113
+ cached = await self._cache.get(f"content:{content_id}")
114
+ if cached:
115
+ return Content.model_validate_json(cached)
116
+ content = await self._repo.get(content_id)
117
+ await self._cache.set(f"content:{content_id}", content.model_dump_json())
118
+ return content
119
+ ```
120
+
121
+ ### Concurrency with gather
122
+
123
+ ```python
124
+ # Pattern: Parallel async operations
125
+ async def enrich_sources(sources: list[Source]) -> list[EnrichedSource]:
126
+ tasks = [enrich_single(s) for s in sources]
127
+ return await asyncio.gather(*tasks, return_exceptions=True)
128
+ ```
129
+
130
+ ### Async Context Managers
131
+
132
+ ```python
133
+ from contextlib import asynccontextmanager
134
+ from collections.abc import AsyncIterator
135
+
136
+ @asynccontextmanager
137
+ async def db_session() -> AsyncIterator[AsyncSession]:
138
+ session = AsyncSession(engine)
139
+ try:
140
+ yield session
141
+ await session.commit()
142
+ except Exception:
143
+ await session.rollback()
144
+ raise
145
+ finally:
146
+ await session.close()
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Error Handling
152
+
153
+ ### Exception Hierarchy
154
+
155
+ ```python
156
+ # Pattern: Domain-specific exception hierarchy
157
+ class AppError(Exception):
158
+ """Base application error."""
159
+ def __init__(self, message: str, code: str = "INTERNAL_ERROR") -> None:
160
+ self.message = message
161
+ self.code = code
162
+ super().__init__(message)
163
+
164
+ class NotFoundError(AppError):
165
+ def __init__(self, resource: str, id: str) -> None:
166
+ super().__init__(f"{resource} {id} not found", code="NOT_FOUND")
167
+
168
+ class ValidationError(AppError):
169
+ def __init__(self, details: list[str]) -> None:
170
+ self.details = details
171
+ super().__init__("; ".join(details), code="VALIDATION_ERROR")
172
+
173
+ class ExternalServiceError(AppError):
174
+ def __init__(self, service: str, message: str) -> None:
175
+ super().__init__(f"{service}: {message}", code="EXTERNAL_ERROR")
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Result Pattern
181
+
182
+ ```python
183
+ from dataclasses import dataclass
184
+ from typing import Generic, TypeVar
185
+
186
+ T = TypeVar("T")
187
+
188
+ @dataclass(frozen=True)
189
+ class Result(Generic[T]):
190
+ value: T | None = None
191
+ error: str | None = None
192
+
193
+ @property
194
+ def is_ok(self) -> bool:
195
+ return self.error is None
196
+
197
+ @classmethod
198
+ def ok(cls, value: T) -> "Result[T]":
199
+ return cls(value=value)
200
+
201
+ @classmethod
202
+ def fail(cls, error: str) -> "Result[T]":
203
+ return cls(error=error)
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Naming Conventions
209
+
210
+ | Type | Pattern | Example |
211
+ |------|---------|---------|
212
+ | Modules | snake_case | `user_service.py`, `auth_middleware.py` |
213
+ | Classes | PascalCase | `UserService`, `CreateUserRequest` |
214
+ | Functions | snake_case | `get_user_by_email()` |
215
+ | Constants | UPPER_SNAKE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
216
+ | Type aliases | PascalCase | `type UserId = int` |
217
+ | Private | Leading underscore | `_validate_input()`, `_cache` |
218
+ | Protocols | PascalCase, noun | `Repository`, `EventPublisher` |
219
+
220
+ ---
221
+
222
+ ## Code Style Principles
223
+
224
+ 1. **Explicit over implicit**: Type all function signatures
225
+ 2. **Composition over inheritance**: Prefer protocols and dependency injection
226
+ 3. **Immutable by default**: Use `frozen=True` dataclasses, avoid mutation
227
+ 4. **Fail fast**: Validate at boundaries, propagate errors clearly
228
+ 5. **Flat is better**: Avoid deep nesting; use early returns and guard clauses
229
+
230
+ ---
231
+
232
+ _Document patterns, not every function. Code should be typed, testable, and explicit._