@malamute/ai-rules 1.0.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 (46) hide show
  1. package/README.md +174 -0
  2. package/bin/cli.js +5 -0
  3. package/configs/_shared/.claude/commands/fix-issue.md +38 -0
  4. package/configs/_shared/.claude/commands/generate-tests.md +49 -0
  5. package/configs/_shared/.claude/commands/review-pr.md +77 -0
  6. package/configs/_shared/.claude/rules/accessibility.md +270 -0
  7. package/configs/_shared/.claude/rules/performance.md +226 -0
  8. package/configs/_shared/.claude/rules/security.md +188 -0
  9. package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
  10. package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
  11. package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
  12. package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
  13. package/configs/_shared/CLAUDE.md +174 -0
  14. package/configs/angular/.claude/rules/components.md +257 -0
  15. package/configs/angular/.claude/rules/state.md +250 -0
  16. package/configs/angular/.claude/rules/testing.md +422 -0
  17. package/configs/angular/.claude/settings.json +31 -0
  18. package/configs/angular/CLAUDE.md +251 -0
  19. package/configs/dotnet/.claude/rules/api.md +370 -0
  20. package/configs/dotnet/.claude/rules/architecture.md +199 -0
  21. package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
  22. package/configs/dotnet/.claude/rules/testing.md +389 -0
  23. package/configs/dotnet/.claude/settings.json +9 -0
  24. package/configs/dotnet/CLAUDE.md +319 -0
  25. package/configs/nestjs/.claude/rules/auth.md +321 -0
  26. package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
  27. package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
  28. package/configs/nestjs/.claude/rules/modules.md +215 -0
  29. package/configs/nestjs/.claude/rules/testing.md +315 -0
  30. package/configs/nestjs/.claude/rules/validation.md +279 -0
  31. package/configs/nestjs/.claude/settings.json +15 -0
  32. package/configs/nestjs/CLAUDE.md +263 -0
  33. package/configs/nextjs/.claude/rules/components.md +211 -0
  34. package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
  35. package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
  36. package/configs/nextjs/.claude/rules/testing.md +315 -0
  37. package/configs/nextjs/.claude/settings.json +29 -0
  38. package/configs/nextjs/CLAUDE.md +376 -0
  39. package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
  40. package/configs/python/.claude/rules/fastapi.md +272 -0
  41. package/configs/python/.claude/rules/flask.md +332 -0
  42. package/configs/python/.claude/rules/testing.md +374 -0
  43. package/configs/python/.claude/settings.json +18 -0
  44. package/configs/python/CLAUDE.md +273 -0
  45. package/package.json +41 -0
  46. package/src/install.js +315 -0
@@ -0,0 +1,355 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # SQLAlchemy 2.0 Rules
7
+
8
+ ## Async Setup
9
+
10
+ ```python
11
+ from sqlalchemy.ext.asyncio import (
12
+ create_async_engine,
13
+ AsyncSession,
14
+ async_sessionmaker,
15
+ )
16
+ from sqlalchemy.orm import DeclarativeBase
17
+
18
+ DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/db"
19
+
20
+ engine = create_async_engine(DATABASE_URL, echo=False)
21
+
22
+ async_session_maker = async_sessionmaker(
23
+ engine,
24
+ class_=AsyncSession,
25
+ expire_on_commit=False,
26
+ )
27
+
28
+ class Base(DeclarativeBase):
29
+ pass
30
+ ```
31
+
32
+ ## Model Definitions (SQLAlchemy 2.0 style)
33
+
34
+ ```python
35
+ from datetime import datetime
36
+ from sqlalchemy import String, ForeignKey, func
37
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
38
+
39
+ class TimestampMixin:
40
+ created_at: Mapped[datetime] = mapped_column(
41
+ default=func.now(),
42
+ server_default=func.now(),
43
+ )
44
+ updated_at: Mapped[datetime | None] = mapped_column(
45
+ onupdate=func.now(),
46
+ default=None,
47
+ )
48
+
49
+ class User(TimestampMixin, Base):
50
+ __tablename__ = "users"
51
+
52
+ id: Mapped[int] = mapped_column(primary_key=True)
53
+ email: Mapped[str] = mapped_column(String(256), unique=True, index=True)
54
+ password_hash: Mapped[str] = mapped_column(String(256))
55
+ name: Mapped[str | None] = mapped_column(String(100))
56
+ role: Mapped[str] = mapped_column(String(50), default="user")
57
+ is_active: Mapped[bool] = mapped_column(default=True)
58
+
59
+ # Relationships
60
+ posts: Mapped[list["Post"]] = relationship(
61
+ back_populates="author",
62
+ lazy="selectin",
63
+ )
64
+
65
+ def __repr__(self) -> str:
66
+ return f"<User(id={self.id}, email={self.email})>"
67
+
68
+
69
+ class Post(TimestampMixin, Base):
70
+ __tablename__ = "posts"
71
+
72
+ id: Mapped[int] = mapped_column(primary_key=True)
73
+ title: Mapped[str] = mapped_column(String(200))
74
+ content: Mapped[str | None]
75
+ published: Mapped[bool] = mapped_column(default=False)
76
+
77
+ # Foreign key
78
+ author_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
79
+ author: Mapped["User"] = relationship(back_populates="posts")
80
+ ```
81
+
82
+ ## Repository Pattern
83
+
84
+ ```python
85
+ from sqlalchemy import select, delete, update
86
+ from sqlalchemy.ext.asyncio import AsyncSession
87
+
88
+ class BaseRepository[T]:
89
+ def __init__(self, session: AsyncSession, model: type[T]):
90
+ self.session = session
91
+ self.model = model
92
+
93
+ async def get(self, id: int) -> T | None:
94
+ return await self.session.get(self.model, id)
95
+
96
+ async def get_all(
97
+ self,
98
+ skip: int = 0,
99
+ limit: int = 100,
100
+ ) -> list[T]:
101
+ stmt = select(self.model).offset(skip).limit(limit)
102
+ result = await self.session.execute(stmt)
103
+ return list(result.scalars().all())
104
+
105
+ async def create(self, **kwargs) -> T:
106
+ instance = self.model(**kwargs)
107
+ self.session.add(instance)
108
+ await self.session.flush()
109
+ await self.session.refresh(instance)
110
+ return instance
111
+
112
+ async def update(self, id: int, **kwargs) -> T | None:
113
+ stmt = (
114
+ update(self.model)
115
+ .where(self.model.id == id)
116
+ .values(**kwargs)
117
+ .returning(self.model)
118
+ )
119
+ result = await self.session.execute(stmt)
120
+ return result.scalar_one_or_none()
121
+
122
+ async def delete(self, id: int) -> bool:
123
+ stmt = delete(self.model).where(self.model.id == id)
124
+ result = await self.session.execute(stmt)
125
+ return result.rowcount > 0
126
+
127
+
128
+ class UserRepository(BaseRepository[User]):
129
+ def __init__(self, session: AsyncSession):
130
+ super().__init__(session, User)
131
+
132
+ async def get_by_email(self, email: str) -> User | None:
133
+ stmt = select(User).where(User.email == email)
134
+ result = await self.session.execute(stmt)
135
+ return result.scalar_one_or_none()
136
+
137
+ async def get_active_users(self) -> list[User]:
138
+ stmt = select(User).where(User.is_active == True)
139
+ result = await self.session.execute(stmt)
140
+ return list(result.scalars().all())
141
+
142
+ async def search(
143
+ self,
144
+ query: str,
145
+ skip: int = 0,
146
+ limit: int = 20,
147
+ ) -> list[User]:
148
+ stmt = (
149
+ select(User)
150
+ .where(
151
+ User.email.ilike(f"%{query}%") |
152
+ User.name.ilike(f"%{query}%")
153
+ )
154
+ .offset(skip)
155
+ .limit(limit)
156
+ )
157
+ result = await self.session.execute(stmt)
158
+ return list(result.scalars().all())
159
+ ```
160
+
161
+ ## Query Patterns
162
+
163
+ ### Select Queries
164
+
165
+ ```python
166
+ from sqlalchemy import select, and_, or_, func
167
+
168
+ # Basic select
169
+ stmt = select(User).where(User.is_active == True)
170
+
171
+ # Multiple conditions
172
+ stmt = select(User).where(
173
+ and_(
174
+ User.is_active == True,
175
+ User.role == "admin",
176
+ )
177
+ )
178
+
179
+ # OR conditions
180
+ stmt = select(User).where(
181
+ or_(
182
+ User.email.contains("@company.com"),
183
+ User.role == "admin",
184
+ )
185
+ )
186
+
187
+ # Ordering
188
+ stmt = select(User).order_by(User.created_at.desc())
189
+
190
+ # Aggregation
191
+ stmt = select(func.count(User.id)).where(User.is_active == True)
192
+ result = await session.execute(stmt)
193
+ count = result.scalar_one()
194
+ ```
195
+
196
+ ### Joins and Relationships
197
+
198
+ ```python
199
+ from sqlalchemy.orm import selectinload, joinedload
200
+
201
+ # Eager load relationships (N+1 prevention)
202
+ stmt = (
203
+ select(User)
204
+ .options(selectinload(User.posts))
205
+ .where(User.id == user_id)
206
+ )
207
+
208
+ # Join for filtering
209
+ stmt = (
210
+ select(User)
211
+ .join(User.posts)
212
+ .where(Post.published == True)
213
+ .distinct()
214
+ )
215
+
216
+ # Left outer join
217
+ stmt = (
218
+ select(User, func.count(Post.id).label("post_count"))
219
+ .outerjoin(User.posts)
220
+ .group_by(User.id)
221
+ )
222
+ ```
223
+
224
+ ### Pagination
225
+
226
+ ```python
227
+ from sqlalchemy import select, func
228
+
229
+ async def get_paginated(
230
+ session: AsyncSession,
231
+ page: int = 1,
232
+ size: int = 20,
233
+ ) -> tuple[list[User], int]:
234
+ # Count total
235
+ count_stmt = select(func.count(User.id))
236
+ total = await session.scalar(count_stmt)
237
+
238
+ # Get items
239
+ stmt = (
240
+ select(User)
241
+ .order_by(User.id)
242
+ .offset((page - 1) * size)
243
+ .limit(size)
244
+ )
245
+ result = await session.execute(stmt)
246
+ items = list(result.scalars().all())
247
+
248
+ return items, total
249
+ ```
250
+
251
+ ## Transactions
252
+
253
+ ```python
254
+ from sqlalchemy.exc import IntegrityError
255
+
256
+ # Automatic transaction (recommended)
257
+ async with async_session_maker() as session:
258
+ user = User(email="test@example.com")
259
+ session.add(user)
260
+ await session.commit() # Commits transaction
261
+
262
+ # Manual transaction control
263
+ async with async_session_maker() as session:
264
+ async with session.begin():
265
+ user = User(email="test@example.com")
266
+ session.add(user)
267
+ # Commits automatically at end of block
268
+
269
+ # Nested transaction (savepoint)
270
+ async with async_session_maker() as session:
271
+ async with session.begin():
272
+ session.add(user1)
273
+
274
+ try:
275
+ async with session.begin_nested():
276
+ session.add(user2)
277
+ raise ValueError("Rollback nested only")
278
+ except ValueError:
279
+ pass # user2 rolled back, user1 still pending
280
+
281
+ await session.commit() # Only user1 committed
282
+ ```
283
+
284
+ ## Alembic Migrations
285
+
286
+ ```bash
287
+ # Initialize
288
+ alembic init alembic
289
+
290
+ # Generate migration
291
+ alembic revision --autogenerate -m "Add users table"
292
+
293
+ # Apply migrations
294
+ alembic upgrade head
295
+
296
+ # Rollback
297
+ alembic downgrade -1
298
+
299
+ # Show current revision
300
+ alembic current
301
+ ```
302
+
303
+ ### Async Alembic Config
304
+
305
+ ```python
306
+ # alembic/env.py
307
+ import asyncio
308
+ from sqlalchemy.ext.asyncio import create_async_engine
309
+
310
+ def run_migrations_online():
311
+ connectable = create_async_engine(settings.database_url)
312
+
313
+ async def do_run_migrations():
314
+ async with connectable.connect() as connection:
315
+ await connection.run_sync(do_run_migrations_sync)
316
+ await connectable.dispose()
317
+
318
+ def do_run_migrations_sync(connection):
319
+ context.configure(
320
+ connection=connection,
321
+ target_metadata=Base.metadata,
322
+ )
323
+ with context.begin_transaction():
324
+ context.run_migrations()
325
+
326
+ asyncio.run(do_run_migrations())
327
+ ```
328
+
329
+ ## Soft Delete
330
+
331
+ ```python
332
+ from datetime import datetime
333
+ from sqlalchemy import event
334
+
335
+ class SoftDeleteMixin:
336
+ deleted_at: Mapped[datetime | None] = mapped_column(default=None)
337
+
338
+ @property
339
+ def is_deleted(self) -> bool:
340
+ return self.deleted_at is not None
341
+
342
+ def soft_delete(self) -> None:
343
+ self.deleted_at = datetime.utcnow()
344
+
345
+ class User(SoftDeleteMixin, Base):
346
+ __tablename__ = "users"
347
+ # ...
348
+
349
+ # Query filter for soft delete
350
+ def not_deleted():
351
+ return User.deleted_at.is_(None)
352
+
353
+ # Usage
354
+ stmt = select(User).where(not_deleted())
355
+ ```
@@ -0,0 +1,272 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Rules
7
+
8
+ ## Router Structure
9
+
10
+ ```python
11
+ from fastapi import APIRouter, Depends, status
12
+ from typing import Annotated
13
+
14
+ router = APIRouter()
15
+
16
+ @router.get(
17
+ "/",
18
+ response_model=list[UserResponse],
19
+ summary="List all users",
20
+ description="Get a paginated list of users",
21
+ )
22
+ async def list_users(
23
+ service: UserServiceDep,
24
+ skip: int = 0,
25
+ limit: int = 100,
26
+ ) -> list[UserResponse]:
27
+ return await service.get_all(skip=skip, limit=limit)
28
+
29
+
30
+ @router.get(
31
+ "/{user_id}",
32
+ response_model=UserResponse,
33
+ responses={404: {"description": "User not found"}},
34
+ )
35
+ async def get_user(
36
+ user_id: int,
37
+ service: UserServiceDep,
38
+ ) -> UserResponse:
39
+ return await service.get_by_id(user_id)
40
+
41
+
42
+ @router.post(
43
+ "/",
44
+ response_model=UserResponse,
45
+ status_code=status.HTTP_201_CREATED,
46
+ )
47
+ async def create_user(
48
+ user_data: UserCreate,
49
+ service: UserServiceDep,
50
+ ) -> UserResponse:
51
+ return await service.create(user_data)
52
+
53
+
54
+ @router.put("/{user_id}", response_model=UserResponse)
55
+ async def update_user(
56
+ user_id: int,
57
+ user_data: UserUpdate,
58
+ service: UserServiceDep,
59
+ ) -> UserResponse:
60
+ return await service.update(user_id, user_data)
61
+
62
+
63
+ @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
64
+ async def delete_user(
65
+ user_id: int,
66
+ service: UserServiceDep,
67
+ ) -> None:
68
+ await service.delete(user_id)
69
+ ```
70
+
71
+ ## Pydantic Schemas (v2)
72
+
73
+ ```python
74
+ from pydantic import BaseModel, EmailStr, Field, ConfigDict
75
+ from datetime import datetime
76
+
77
+ # Base schema with common config
78
+ class BaseSchema(BaseModel):
79
+ model_config = ConfigDict(from_attributes=True)
80
+
81
+ # Request schemas
82
+ class UserCreate(BaseModel):
83
+ email: EmailStr
84
+ password: str = Field(min_length=8)
85
+ name: str = Field(min_length=1, max_length=100)
86
+
87
+ class UserUpdate(BaseModel):
88
+ email: EmailStr | None = None
89
+ name: str | None = Field(default=None, max_length=100)
90
+
91
+ # Response schemas
92
+ class UserResponse(BaseSchema):
93
+ id: int
94
+ email: EmailStr
95
+ name: str
96
+ created_at: datetime
97
+
98
+ # Nested schemas
99
+ class UserWithPostsResponse(UserResponse):
100
+ posts: list["PostResponse"] = []
101
+
102
+ # Pagination
103
+ class PaginatedResponse[T](BaseModel):
104
+ items: list[T]
105
+ total: int
106
+ page: int
107
+ size: int
108
+ pages: int
109
+ ```
110
+
111
+ ## Dependencies
112
+
113
+ ### Validation with Dependencies
114
+
115
+ ```python
116
+ from fastapi import Depends, HTTPException, status, Path
117
+
118
+ async def valid_user_id(
119
+ user_id: Annotated[int, Path(gt=0)],
120
+ db: DbSession,
121
+ ) -> User:
122
+ """Validate user exists and return it."""
123
+ user = await db.get(User, user_id)
124
+ if not user:
125
+ raise HTTPException(
126
+ status_code=status.HTTP_404_NOT_FOUND,
127
+ detail=f"User {user_id} not found",
128
+ )
129
+ return user
130
+
131
+ ValidUser = Annotated[User, Depends(valid_user_id)]
132
+
133
+ # Usage - user is already validated
134
+ @router.get("/{user_id}")
135
+ async def get_user(user: ValidUser) -> UserResponse:
136
+ return user
137
+ ```
138
+
139
+ ### Authentication Dependency
140
+
141
+ ```python
142
+ from fastapi import Depends, HTTPException, status
143
+ from fastapi.security import OAuth2PasswordBearer
144
+
145
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
146
+
147
+ async def get_current_user(
148
+ token: Annotated[str, Depends(oauth2_scheme)],
149
+ db: DbSession,
150
+ ) -> User:
151
+ credentials_exception = HTTPException(
152
+ status_code=status.HTTP_401_UNAUTHORIZED,
153
+ detail="Could not validate credentials",
154
+ headers={"WWW-Authenticate": "Bearer"},
155
+ )
156
+ try:
157
+ payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
158
+ user_id: int = payload.get("sub")
159
+ if user_id is None:
160
+ raise credentials_exception
161
+ except JWTError:
162
+ raise credentials_exception
163
+
164
+ user = await db.get(User, user_id)
165
+ if user is None:
166
+ raise credentials_exception
167
+ return user
168
+
169
+ CurrentUser = Annotated[User, Depends(get_current_user)]
170
+
171
+ # Role-based dependency
172
+ def require_role(required_role: str):
173
+ async def check_role(user: CurrentUser) -> User:
174
+ if user.role != required_role:
175
+ raise HTTPException(
176
+ status_code=status.HTTP_403_FORBIDDEN,
177
+ detail="Insufficient permissions",
178
+ )
179
+ return user
180
+ return check_role
181
+
182
+ AdminUser = Annotated[User, Depends(require_role("admin"))]
183
+ ```
184
+
185
+ ## Background Tasks
186
+
187
+ ```python
188
+ from fastapi import BackgroundTasks
189
+
190
+ async def send_welcome_email(email: str, name: str) -> None:
191
+ # Async email sending
192
+ ...
193
+
194
+ @router.post("/", status_code=status.HTTP_201_CREATED)
195
+ async def create_user(
196
+ user_data: UserCreate,
197
+ background_tasks: BackgroundTasks,
198
+ service: UserServiceDep,
199
+ ) -> UserResponse:
200
+ user = await service.create(user_data)
201
+ background_tasks.add_task(send_welcome_email, user.email, user.name)
202
+ return user
203
+ ```
204
+
205
+ ## Middleware
206
+
207
+ ```python
208
+ from fastapi import FastAPI, Request
209
+ from starlette.middleware.cors import CORSMiddleware
210
+ import time
211
+
212
+ app = FastAPI()
213
+
214
+ # CORS
215
+ app.add_middleware(
216
+ CORSMiddleware,
217
+ allow_origins=["http://localhost:3000"],
218
+ allow_credentials=True,
219
+ allow_methods=["*"],
220
+ allow_headers=["*"],
221
+ )
222
+
223
+ # Custom middleware
224
+ @app.middleware("http")
225
+ async def add_process_time_header(request: Request, call_next):
226
+ start_time = time.perf_counter()
227
+ response = await call_next(request)
228
+ process_time = time.perf_counter() - start_time
229
+ response.headers["X-Process-Time"] = str(process_time)
230
+ return response
231
+ ```
232
+
233
+ ## Lifespan Events
234
+
235
+ ```python
236
+ from contextlib import asynccontextmanager
237
+ from fastapi import FastAPI
238
+
239
+ @asynccontextmanager
240
+ async def lifespan(app: FastAPI):
241
+ # Startup
242
+ await init_db()
243
+ print("Application started")
244
+
245
+ yield
246
+
247
+ # Shutdown
248
+ await close_db()
249
+ print("Application stopped")
250
+
251
+ app = FastAPI(lifespan=lifespan)
252
+ ```
253
+
254
+ ## OpenAPI Documentation
255
+
256
+ ```python
257
+ app = FastAPI(
258
+ title="My API",
259
+ description="API description with **markdown** support",
260
+ version="1.0.0",
261
+ docs_url="/docs",
262
+ redoc_url="/redoc",
263
+ openapi_tags=[
264
+ {"name": "users", "description": "User operations"},
265
+ {"name": "auth", "description": "Authentication"},
266
+ ],
267
+ )
268
+
269
+ # Disable docs in production
270
+ if settings.environment == "production":
271
+ app = FastAPI(docs_url=None, redoc_url=None)
272
+ ```