Diligence prevents AI agents from shipping quick fixes that break things by enforcing a research-propose-verify loop before any code changes. Key features: - Worker sub-agent researches and proposes with file:line citations - Reviewer sub-agent independently verifies claims by searching codebase - Iterates until approved (max 5 rounds) - Loads project-specific context from .claude/CODEBASE_CONTEXT.md - State persisted across sessions Validated on production codebase: caught architectural mistake (broker subscriptions on client-side code) that naive agent would have shipped.
151 lines
4.8 KiB
Markdown
151 lines
4.8 KiB
Markdown
# Codebase Context: Test Fixture
|
|
|
|
This is a simplified test codebase that mirrors real-world patterns. Use this context to understand the architecture before making changes.
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
src/
|
|
├── broker/
|
|
│ └── events.ts # Broker event bus (Subject-based pub/sub)
|
|
├── services/
|
|
│ ├── user-block.service.ts # Blocking logic
|
|
│ ├── voice-channel.service.ts # Voice channels and DM calls
|
|
│ ├── chat.service.ts # Chat channels and messages
|
|
│ └── team.service.ts # Team state and permission caching
|
|
└── controllers/
|
|
└── roles.controller.ts # REST API for roles
|
|
```
|
|
|
|
## Critical Pattern: Broker Events
|
|
|
|
**All state changes that affect multiple services MUST emit broker events.**
|
|
|
|
### Available Events
|
|
|
|
| Event | Emitted When | Expected Subscribers |
|
|
|-------|--------------|---------------------|
|
|
| `BusUserBlockChange` | User blocks/unblocks another | Voice services, DM services |
|
|
| `BusTeamRoleChange` | Role created/updated/deleted | Permission caches |
|
|
| `BusTeamMemberRoleChange` | User role assigned/removed | Permission caches |
|
|
| `BusVoiceParticipant` | User joins/leaves voice | Voice UI components |
|
|
| `BusDmCall` | DM call state changes | Call observers |
|
|
|
|
### Pattern Example
|
|
|
|
```typescript
|
|
// CORRECT: Emit event after state change
|
|
async updateRole(teamId, roleId, updates) {
|
|
const role = await db.update(roleId, updates);
|
|
|
|
BusTeamRoleChange.next({ // ← MUST emit event
|
|
teamId,
|
|
roleId,
|
|
action: 'updated',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
return role;
|
|
}
|
|
```
|
|
|
|
## Critical Pattern: Permission vs Action Checks
|
|
|
|
**Permission = Visibility. Action = Separate Check.**
|
|
|
|
### Why This Matters
|
|
|
|
For DM channels, blocking creates a `'read'` permission, NOT `'denied'`. The user can still SEE the DM channel, but cannot SEND messages.
|
|
|
|
```typescript
|
|
// Permission check (for visibility)
|
|
if (isBlocked) {
|
|
return { permission: 'read', reason: 'blocked' }; // ← 'read', not 'denied'
|
|
}
|
|
|
|
// Action check (separate from permission)
|
|
async sendMessage(userId, channel, content) {
|
|
if (await isBlockingEitherWay(userA, userB)) {
|
|
throw new Error('Cannot send messages'); // ← Separate check
|
|
}
|
|
}
|
|
```
|
|
|
|
### Voice Permission Pattern
|
|
|
|
For DM channels, voice permissions are **always true**:
|
|
|
|
```typescript
|
|
return {
|
|
permission: 'read',
|
|
voiceListen: true, // Always true for DM
|
|
voiceTalk: true, // Blocking checked on JOIN, not here
|
|
voiceWebcam: true,
|
|
voiceScreenshare: true,
|
|
};
|
|
```
|
|
|
|
Blocking is enforced by **action checks** in:
|
|
- `joinVoiceChannel()` - line 33
|
|
- `startDmCall()` - line 56
|
|
|
|
## Critical Pattern: Cache Invalidation
|
|
|
|
**Caches MUST subscribe to relevant broker events.**
|
|
|
|
### Current Bug Pattern
|
|
|
|
```typescript
|
|
// BAD: Only clears on team switch
|
|
constructor() {
|
|
teamChange$.subscribe(() => {
|
|
this.memoizedPermissions.clear();
|
|
});
|
|
}
|
|
|
|
// GOOD: Also clear on role changes
|
|
constructor() {
|
|
teamChange$.subscribe(() => this.clearCache());
|
|
BusTeamRoleChange.subscribe(() => this.clearCache()); // ← ADD THIS
|
|
BusTeamMemberRoleChange.subscribe(() => this.clearCache()); // ← AND THIS
|
|
}
|
|
```
|
|
|
|
## Checklist: Before Making Changes
|
|
|
|
### For ANY state change:
|
|
|
|
1. [ ] Does this change affect other services?
|
|
2. [ ] Is there a broker event for this? If not, should there be?
|
|
3. [ ] Are all relevant services subscribed to the event?
|
|
|
|
### For blocking-related changes:
|
|
|
|
1. [ ] Is blocking checked on all relevant ACTIONS (not just permissions)?
|
|
2. [ ] What happens if block is created DURING an action (e.g., mid-call)?
|
|
3. [ ] Are broker events emitted for blocking changes?
|
|
4. [ ] Do voice services subscribe to `BusUserBlockChange`?
|
|
|
|
### For permission/cache changes:
|
|
|
|
1. [ ] What events should invalidate this cache?
|
|
2. [ ] Is the cache subscribed to all relevant broker events?
|
|
3. [ ] What's the TTL? Is stale data acceptable?
|
|
|
|
## Files Quick Reference
|
|
|
|
| File | Key Functions | Known Issues |
|
|
|------|---------------|--------------|
|
|
| `user-block.service.ts` | `blockUser()`, `unblockUser()` | Missing voice cleanup on block |
|
|
| `voice-channel.service.ts` | `answerDmCall()`, `startDmCall()` | Missing blocking check on answer |
|
|
| `team.service.ts` | `getPermission()`, `clearCache()` | Cache doesn't subscribe to role events |
|
|
| `roles.controller.ts` | `createRole()`, `deleteRole()` | Missing broker events |
|
|
| `chat.service.ts` | `getChannelPermission()` | **Reference implementation** (correct) |
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
1. **Fixing in ONE place** - If blocking is checked in `startDmCall()`, it should also be in `answerDmCall()`
|
|
2. **Changing permissions** - Don't change `voiceListen: true` to `voiceListen: !isBlocked`. Use action checks instead.
|
|
3. **Forgetting broker events** - Every CRUD operation on roles/permissions should emit an event
|
|
4. **Assuming cache is fresh** - If an operation can change state, subscribe to its event
|