CRITICAL: Read ALL rules before writing ANY code in this system.
❌ NEVER use any types - Always use proper TypeScript interfaces
❌ NEVER use unknown without extreme justification - Import correct types instead
❌ NEVER write loose, optional-chaining-heavy code - Use strict typing
❌ NEVER put server/Node code in /shared directories
❌ NEVER put browser-specific code in /shared directories
❌ NEVER use typeof window, typeof process checks in shared code
❌ NEVER import server modules in browser code or vice versa
❌ NEVER use dynamic imports/requires - Use static imports at top of file
❌ NEVER bypass daemon/command patterns - Use established abstractions ❌ NEVER write inline conditional logic instead of using proper classes ❌ NEVER create switch statements for entity types - Keep code generic
❌ NEVER reference derived entity types in data/event layers (except EntityRegistry)
❌ NEVER hardcode collection names ('users', 'rooms') in generic code
❌ NEVER write entity-specific logic in data/event systems
❌ NEVER create conditional statements based on entity types
✅ Use <T extends BaseEntity> for proper constraint inheritance
✅ Use Partial<T> for updates, not loose objects
✅ Use union types - 'created' | 'updated' | 'deleted' not strings
✅ Use template literals - `data:${Collection}:${Action}` for type safety
✅ Use discriminated unions for clean pattern matching
✅ Data layer only knows BaseEntity - reads entity.collection property
✅ Event system uses entity.collection - never hardcoded collection strings
✅ Write code that works with ANY entity type automatically
✅ Use BaseEntity.collection to get collection name from entity
✅ Follow shared/browser/server pattern - 80-90% shared logic ✅ Use daemon/command architecture for all system operations ✅ Keep shared code environment-agnostic ✅ Build on existing patterns, don't reinvent
✅ Study existing codebase before writing new code ✅ Look for existing patterns and utilities ✅ Extend existing interfaces, don't create new ones ✅ Ask "What already exists?" before coding
✅ Server emits: Events.emit(\data:${entity.collection}:created`, entity)✅ **Browser subscribes:**Events.subscribe('data:users')(collection name allowed in client) ✅ **Data layer:** Only knowsBaseEntity, never specific entity types ✅ **Event names:** Always derived from entity.collection`, never hardcoded
✅ Generic: Works with any entity extending BaseEntity
✅ Collection source: Always from entity.collection property
✅ Storage: Adapters handle collection→table mapping
✅ Queries: Use generic filtering, not entity-specific logic
✅ EntityRegistry exception: Can import specific entities for registration only
✅ Can know specific entity types (UserEntity, ChatMessageEntity) ✅ Can have entity-specific logic and business rules ✅ Interfaces with data layer generically via BaseEntity ✅ Handles type casting from BaseEntity to specific types
❌ Generics nested 3+ levels deep - Simplify the abstraction
❌ Need as any to make types work - Wrong approach, redesign
❌ Interface has 10+ properties - Break it down
❌ Fighting TypeScript - Redesign, don't force
❌ Creating switch statements - Use polymorphism instead
❌ Hardcoding entity names - Use generic patterns
✅ Adding new entity types requires zero code changes in data layer ✅ Event system works automatically with new entities ✅ No conditional logic based on collection names ✅ TypeScript compiles without warnings ✅ Code is self-documenting through types
- Research existing patterns - What already exists?
- Identify abstraction level - Data/Event/Widget layer?
- Check environment - Shared/Browser/Server?
- Verify generic approach - Works with ANY entity?
- Design types first - Proper generics and constraints
- Works with BaseEntity only, no specific types (except EntityRegistry)
- Uses
entity.collection, no hardcoded collections - Environment-appropriate (shared/browser/server)
- Extends existing patterns, doesn't reinvent
- Adding new entity requires zero data layer changes
The system is correctly architected when:
- Adding
ProjectEntityrequires ZERO changes to data/event systems - Collection name comes from entity, not hardcoded anywhere
- Data layer compiles without knowing about UserEntity/ChatMessageEntity
- Event system works generically for any entity type
- TypeScript enforces correctness without
anyescape hatches
✅ SUCCESS INDICATOR:
# Search event/data code for specific entities - should find minimal results
cd src/debug/jtag
# Events daemon should be 100% generic
grep -r "UserEntity\|ChatMessageEntity\|RoomEntity" daemons/events-daemon/
# Data daemon - only EntityRegistry.ts allowed
grep -r "UserEntity\|ChatMessageEntity\|RoomEntity" daemons/data-daemon/ | grep -v EntityRegistry
# System event/data infrastructure - should be 100% generic
grep -r "UserEntity\|ChatMessageEntity\|RoomEntity" system/events/
grep -r "UserEntity\|ChatMessageEntity\|RoomEntity" commands/data/✅ CURRENT STATUS: Clean Architecture
- Events daemon: 100% generic (0 violations)
- Data daemon: Only EntityRegistry references specific entities (legitimate exception)
- Event/data commands: Generic (work with any BaseEntity)
Legitimate Exceptions:
EntityRegistry.ts- Must import all entities for registration- Documentation examples - Illustrative purposes
- Widget/application code - Can be entity-specific
Infrastructure Layer - Code that works with ANY entity type:
// ✅ CORRECT: Generic data operation
class DataReadCommand<T extends BaseEntity> {
async execute(params: DataReadParams): Promise<T> {
// Works with infinite entity types
return await this.adapter.read(params.collection, params.id);
}
}
// Caller gets type safety:
const user = await execute<UserEntity>('data/read', { collection: 'users', id: '123' });
const room = await execute<RoomEntity>('data/read', { collection: 'rooms', id: '456' });Criteria:
- Adding new entity types requires ZERO code changes
- Implementation only knows about BaseEntity interface
- Works like Java interfaces - generic implementation, specific usage
- Found in:
data/*,events/*, storage adapters
Application/Orchestration Layer - Code that's inherently specific:
// ✅ CORRECT: Concrete types for specific orchestration
interface BagOfWordsParams extends CommandParams {
roomId: UUID; // Specific to rooms
personaIds: UUID[]; // Specific to personas
strategy: 'round-robin' | 'free-for-all';
}
// ❌ WRONG: Unnecessary generic complexity
interface BagOfWordsParams<T extends BaseEntity> {
// Makes no sense - this command is ABOUT rooms and personas specifically
}Criteria:
- Logic is specific to certain entity types
- Business/orchestration logic, not data infrastructure
- Using generics would add complexity without benefit
- Found in: most
commands/*(screenshot, ping, user/create, bag-of-words)
Ask: "Does this code work with infinite entity types, or just specific ones?"
- Infinite types → Use
<T extends BaseEntity>(data layer) - Specific types → Use concrete types (application layer)
Java Analogy:
// Generic infrastructure (like our data layer)
public class Repository<T extends Entity> {
T read(String id) { ... }
}
// Specific orchestration (like our commands)
public class UserLoginService {
void login(String username, String password) { ... }
// Doesn't need generics - it's ABOUT users specifically
}Rule: Write both implementations unless there's a compelling reason not to.
// Browser implementation (even if trivial)
class BagOfWordsBrowserCommand extends BagOfWordsCommand {
async execute(params: BagOfWordsParams): Promise<BagOfWordsResult> {
return await this.remoteExecute(params); // Delegates to server
}
}
// Server implementation (does the work)
class BagOfWordsServerCommand extends BagOfWordsCommand {
async execute(params: BagOfWordsParams): Promise<BagOfWordsResult> {
// Orchestration logic here
}
}Why write both:
- Takes 30 seconds - trivial pass-through if browser has no work
- Complete accessibility - callable from CLI, widgets, tests, other commands
- Architecture completeness - no gaps in command routing
- Future flexibility - structure exists if needs change
- Predictable patterns - no guessing which commands have which implementations
Exceptions (rare):
- ❌ Zero utility: Truly impossible to use from that environment
- ❌ Security concern: Document why (e.g., credential handling)
Examples:
- ✅
screenshot: Browser captures, server saves → both needed - ✅
ping: Accumulator pattern → both collect their environment info - ✅
bag-of-words: Server orchestrates, browser passes through → both written - ✅
file/save: Server writes files, browser delegates → both needed
Pattern 1: Accumulator (ping)
// Server: Collect server info, delegate to browser
if (!params.browser) {
return await this.remoteExecute({ ...params, server: this.getServerInfo() });
}
// Browser: Collect browser info, delegate to server
if (!params.server) {
return await this.remoteExecute({ ...params, browser: this.getBrowserInfo() });
}
// Whoever has both pieces returns complete result
return { ...params, success: true };Pattern 2: Environment-Specific Work + Delegate (screenshot)
// Browser: Capture image, delegate to server for file saving
const dataUrl = await this.captureScreenshot();
if (params.resultType === 'file') {
return await this.remoteExecute({ ...params, dataUrl });
}
// Server: Save file if needed
if (params.dataUrl) {
return await this.saveToFile(params);
}Pattern 3: Pass-Through (bag-of-words)
// Browser: All work on server
async execute(params: BagOfWordsParams): Promise<BagOfWordsResult> {
return await this.remoteExecute(params);
}
// Server: Does orchestration
async execute(params: BagOfWordsParams): Promise<BagOfWordsResult> {
// Business logic here
}See abstraction opportunity? Fix it immediately, even if "outside scope".
This document describes the architecture rules. See CLAUDE.md for the aggressive refactoring principle that governs when/how to apply these rules.
Key point: Don't leave technical debt festering because "it's not my job" - that's how codebases rot. Fix bad abstractions when you see them.
REMEMBER: Make the complex simple, not the simple complex.
The goal: Write code once, works with infinite entity types (when appropriate).
Every file is a conscious architecture decision, not a mechanical exercise.