Purpose: Hard constraints for code generation and development Last Updated: 2026-01-05 (updated for v0.1.0-alpha.5)
CRITICAL: All source files MUST include the AGPL-3.0 license header:
/*
* Copyright (C) 2025 Isekai
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*/Strict Mode: Always enabled
- Use TypeScript for ALL new code
- Avoid
any- use proper types orunknown - Define interfaces for object shapes
- Use type aliases for unions/intersections
// ✅ Good
interface Deviation {
id: string;
title: string;
status: DeviationStatus;
scheduledAt: Date | null;
}
// ❌ Bad
const deviation: any = {...};- Files: kebab-case (
deviation-publisher.ts,auto-scheduler.ts) - Components: PascalCase (
DeviationCard.tsx,ScheduleView.tsx) - Functions: camelCase (
createDeviation,schedulePublish) - Constants: SCREAMING_SNAKE_CASE (
MAX_RETRY_COUNT,DEFAULT_JITTER) - Database fields: snake_case (handled by Prisma
@map) - Environment variables: SCREAMING_SNAKE_CASE
- Semi-colons: Required
- Quotes: Single quotes
- Print width: 100 characters
- Tab width: 2 spaces
- Trailing commas: ES5 style
no-console: OFF (server-side logging allowed)no-unused-vars: WARN@typescript-eslint/no-unused-vars: WARN (ignores_prefixed vars)@typescript-eslint/no-explicit-any: WARNprefer-const: WARNno-var: ERROR
CRITICAL: ALL commits MUST follow Conventional Commits with multi-line messages.
<type>(<scope>): <subject>
<body>
<footer>
Line 1: Type, scope, and brief subject (max 72 characters)
Line 2: Blank line (required)
Line 3+: Detailed body with bullet points explaining changes
Last line: Issue reference if applicable (e.g., Closes #42)
- No emojis - Keep commits professional
- No AI attribution - Commits should appear human-written
- No "Generated with" or "Co-Authored-By" lines
- Always include detailed body - Single-line commits not allowed
- Use imperative mood - "add feature" not "added feature"
- Reference related files - Mention key files changed
feat: New featurefix: Bug fixdocs: Documentation onlystyle: Code formatting (no logic change)refactor: Code refactoringperf: Performance improvementstest: Adding/updating testschore: Maintenance (deps, config)build: Build system changesci: CI/CD changes
Feature Commit:
feat(automation): add weekly quota rule type
Implements weekly_quota rule type that limits posts per calendar week.
- Add maxPostsPerWeek field to AutomationScheduleRule model
- Implement quota evaluation in auto-scheduler
- Add timezone-aware week calculation using date-fns-tz
- Update frontend UI to support weekly quota configuration
- Add validation for maxPostsPerWeek parameter
The weekly quota complements existing daily quota by tracking posts
across Sunday-Saturday periods using the user's timezone.
Closes #42
Bug Fix Commit:
fix(publisher): prevent race condition in execution lock
Fixes race condition where multiple workers could publish same deviation.
- Add executionVersion counter to Deviation model
- Implement atomic lock acquisition with updateMany
- Add lock timeout cleanup job (runs every 30 minutes)
- Update tests to verify single worker acquires lock
The lock now uses UUID-based optimistic locking with version counter
to ensure only one worker can process each deviation.
Fixes #87
Documentation Commit:
docs(api): document browse endpoints and caching strategy
Adds complete documentation for all browse endpoints.
- Document 6 browse modes (home, daily, following, tags, topic, user)
- Explain Redis caching with 5-minute TTL
- Add request coalescing pattern explanation
- Document stale cache fallback strategy
Updates .context/api/endpoints.md with code examples and response formats.
Refactor Commit:
refactor(storage): abstract S3-compatible storage layer
Removes hardcoded Cloudflare R2 usage, adds multi-provider support.
- Extract StorageService interface
- Implement R2StorageService, S3StorageService, MinIOStorageService
- Add getStorageService() factory based on S3_ENDPOINT
- Update all file upload/download calls to use abstraction
- Add S3_PATH_PREFIX for multi-tenant storage isolation
Enables using MinIO for local development and AWS S3 for production
without code changes.
Too short:
feat: add feature
Has emoji:
feat(api): add endpoint 🚀
Has AI attribution:
feat(api): add endpoint
Generated with Claude Code
No body:
fix(bug): fix the bug
Wrong tense:
feat(api): added new endpoint
Rule: Keep publisher worker independent from API
// ✅ Good: Publisher processes jobs independently
// apps/isekai-publisher/src/queues/deviation-publisher.ts
// ❌ Bad: Don't make API HTTP calls from publisher
// Use shared database access insteadRule: ALWAYS use execution locks for deviation publishing
// ✅ Good: UUID-based execution lock
const lockId = randomUUID();
await prisma.deviation.update({
where: {
id: deviationId,
executionLockId: null, // Only if unlocked
},
data: {
executionLockId: lockId,
executionLockedAt: new Date(),
executionVersion: { increment: 1 },
},
});
// ❌ Bad: No lock - allows race conditionsRule: Avoid global mutable state - use dependency injection
// ✅ Good: Inject dependencies
class DeviationPublisher {
constructor(
private prisma: PrismaClient,
private redis: Redis,
private storage: StorageService
) {}
}
// ❌ Bad: Global singletons
const globalPrisma = new PrismaClient();Rule: Use BullMQ queues for operations > 5 seconds
// ✅ Good: Queue long-running publish job
await deviationQueue.add('publish', { deviationId });
// ❌ Bad: Synchronous publishing in HTTP handler
app.post('/api/deviations/:id/publish', async (req, res) => {
await publishToDeviantArt(req.params.id); // Blocks!
res.json({ success: true });
});Rule: Use abstracted StorageService, not direct R2/S3 calls
// ✅ Good: Abstract storage interface
await storageService.upload(file, key);
const url = await storageService.getPresignedUrl(key);
// ❌ Bad: Direct R2 client usage
await r2Client.putObject({ Bucket: 'isekai', Key: key, Body: file });Supported backends: Cloudflare R2, AWS S3, MinIO (S3-compatible)
Rule: Check InstanceSettings for runtime-configurable features
// ✅ Good: Respect instance settings
const settings = await prisma.instanceSettings.findUnique({
where: { id: 'singleton' },
});
if (!settings?.teamInvitesEnabled) {
return res.status(403).json({ error: 'Team invites disabled' });
}
// ❌ Bad: Hardcode feature availability- NEVER log access tokens or refresh tokens
- ALWAYS encrypt tokens at rest (use
packages/shared/src/crypto.ts) - ALWAYS refresh tokens 7 days before expiry (handled by
token-maintenancequeue)
// ✅ Good: Encrypted storage
const encryptedToken = encrypt(accessToken, ENCRYPTION_KEY);
await prisma.user.update({
where: { id: userId },
data: { accessToken: encryptedToken },
});
// ❌ Bad: Plain text tokensRule: Validate ALL user inputs with Zod or Prisma types
// ✅ Good: Zod validation
const schema = z.object({
title: z.string().min(1).max(255),
tags: z.array(z.string()).max(30),
});
const validated = schema.parse(req.body);
// ❌ Bad: Direct use of req.bodyRule: ONLY use Prisma ORM - NEVER raw SQL without parameterization
// ✅ Good: Prisma query
await prisma.deviation.findMany({ where: { userId } });
// ❌ Bad: Raw SQL with string interpolation
await prisma.$queryRaw`SELECT * FROM deviations WHERE user_id = ${userId}`;
// Use $queryRaw`...${Prisma.sql`${userId}`}` if raw SQL is necessaryRule: Use requireAuth or hybridAuth middleware on protected routes
// ✅ Good: Protected route
app.get('/api/deviations', requireAuth, async (req, res) => {
const userId = req.session.userId;
// ...
});
// ❌ Bad: No auth check
app.get('/api/deviations', async (req, res) => {
// Anyone can access!
});Process:
- Edit
packages/shared/prisma/schema.prisma - Run
pnpm db:generateto create migration - Review generated SQL carefully
- Run
pnpm db:migrateto apply - Rebuild shared package:
pnpm --filter @isekai/shared build
- Never edit existing migrations
- Never add NOT NULL columns without default values
- Always add indexes for frequently queried columns
- Test migrations on a database copy first
Rule: Use Prisma onDelete cascade carefully
// ✅ Good: Cascade deletes for owned data
model DeviationFile {
deviation Deviation @relation(fields: [deviationId], references: [id], onDelete: Cascade)
}
// ⚠️ Careful: Set null for referenced data
model Deviation {
automation Automation? @relation(fields: [automationId], references: [id], onDelete: SetNull)
}- Use functional components with hooks
- Extract reusable logic into custom hooks
- Keep components small and focused (< 200 lines)
- Use proper TypeScript prop types
// ✅ Good
interface DeviationCardProps {
deviation: Deviation;
onEdit: (id: string) => void;
showActions?: boolean;
}
export function DeviationCard({ deviation, onEdit, showActions = true }: DeviationCardProps) {
return <Card>...</Card>;
}
// ❌ Bad: No types, too many responsibilities- Local state:
useStatefor component-only state - Global state: Zustand stores for shared state
- Server state: TanStack Query for API data
// ✅ Good: TanStack Query for API data
const { data, isLoading } = useQuery({
queryKey: ['deviations', userId],
queryFn: () => fetchDeviations(userId),
});
// ❌ Bad: Manual fetch with useState
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/deviations')
.then((r) => r.json())
.then(setData);
}, []);- Primary: Tailwind utility classes
- Components: shadcn/ui components
- Avoid: Inline styles, CSS modules
// ✅ Good: Tailwind utilities
<div className="flex items-center gap-4 p-6 bg-background border border-border rounded-lg">
<Avatar src={user.avatarUrl} />
<h3 className="font-semibold">{user.username}</h3>
</div>
// ❌ Bad: Inline styles
<div style={{ display: 'flex', padding: '24px' }}>...</div>Minimum: 30% coverage (current baseline) Target: 80% coverage for critical paths
// ✅ Good: Descriptive test names
describe('DeviationCard', () => {
it('should display deviation title and status', () => {
const deviation = { id: '1', title: 'Test', status: 'draft' };
render(<DeviationCard deviation={deviation} />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
// ❌ Bad: Unclear test names
it('works', () => { /* ... */ });- Mock external APIs (DeviantArt, R2)
- Mock Prisma in unit tests
- Use actual database in integration tests
// ✅ Good: Structured error response
return res.status(400).json({
error: 'Invalid deviation status',
code: 'INVALID_STATUS',
details: { allowed: ['draft', 'scheduled'], received: status },
});
// ❌ Bad: String error
return res.status(400).send('Bad request');- Use structured logging (JSON format)
- Include context (userId, deviationId, etc.)
- Never log sensitive data (tokens, passwords)
// ✅ Good
console.log(
JSON.stringify({
level: 'error',
message: 'Failed to publish deviation',
deviationId,
userId,
error: err.message,
})
);
// ❌ Bad
console.log('Error:', err); // Unstructured, may leak tokens- Use
selectto limit fields returned - Use
includesparingly - prefer separate queries for optional data - Add indexes for frequently filtered/sorted columns
// ✅ Good: Select only needed fields
const deviation = await prisma.deviation.findUnique({
where: { id },
select: { id: true, title: true, status: true },
});
// ❌ Bad: Fetch all fields
const deviation = await prisma.deviation.findUnique({ where: { id } });- Use Redis for frequently accessed data (galleries, browse cache)
- Set appropriate TTLs (5 minutes default, 2 hours stale)
- Implement stale-while-revalidate pattern
Rule: ALWAYS paginate list endpoints
// ✅ Good: Cursor-based pagination
const deviations = await prisma.deviation.findMany({
where: { userId },
take: 20,
cursor: lastId ? { id: lastId } : undefined,
orderBy: { createdAt: 'desc' },
});
// ❌ Bad: No pagination
const deviations = await prisma.deviation.findMany({ where: { userId } });apps/isekai-backend/
├── src/
│ ├── routes/ # Express route handlers (1 file per resource)
│ ├── middleware/ # Auth, rate limiting, error handling
│ ├── lib/ # Shared utilities (deviantart-api, storage, redis)
│ ├── queues/ # BullMQ queue setup
│ └── jobs/ # (deprecated - moved to publisher)
apps/isekai-publisher/
├── src/
│ ├── queues/ # Queue processors (deviation-publisher, token-maintenance)
│ ├── jobs/ # Background jobs (auto-scheduler, recovery jobs)
│ ├── lib/ # Publisher-specific utilities
│ └── index.ts # Entry point with graceful shutdown
apps/isekai-frontend/
├── src/
│ ├── pages/ # React Router pages
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Frontend utilities
│ └── store/ # Zustand stores
packages/shared/
├── prisma/
│ └── schema.prisma # Single source of truth for database schema
├── src/
│ ├── index.ts # Shared types export
│ ├── crypto.ts # Encryption utilities
│ └── publisher/ # Publisher queue types
Approved packages: See .context/architecture/dependencies.md
License compatibility: All dependencies MUST be AGPL-3.0 compatible
Adding dependencies:
- Check license compatibility
- Evaluate bundle size impact (frontend)
- Consider security/maintenance status
- Document in
architecture/dependencies.md
- Node.js: 20+ (LTS)
- pnpm: 9+
- PostgreSQL: 16+
- Redis: 7+
- TypeScript: 5.6+
Rule: Use InstanceSettings and InstanceUser models for SaaS features
// ✅ Good: Check instance role
const instanceUser = await prisma.instanceUser.findUnique({
where: { daUserId: user.deviantartId },
});
if (instanceUser?.role !== 'admin') {
return res.status(403).json({ error: 'Admin only' });
}
// ✅ Good: Use S3_PATH_PREFIX for multi-tenant storage
const storageKey = `${S3_PATH_PREFIX || ''}deviations/${deviationId}/${filename}`;- NEVER modify execution lock logic without thorough review
- NEVER skip token refresh mechanism
- NEVER bypass circuit breaker protection
- NEVER make HTTP calls from publisher to backend API
- NEVER commit
.envfiles or secrets - NEVER push directly to
mainbranch - NEVER merge PRs without passing CI checks
- NEVER use
docker-compose upin production (usedocker-compose.ymlfor dev only)
- Code follows style guidelines
- TypeScript compiles without errors
- ESLint passes (warnings acceptable)
- No console.logs in production code (unless intentional logging)
- Commit message follows Conventional Commits
- License header present in new files
- Tests added for new features
- Documentation updated if needed
- See
.context/anti-patterns.mdfor common mistakes - See
.context/boundaries.mdfor modification limits - See
.context/workflows.mdfor development process - See
.context/guidelines.mdfor PR review checklist