RAG expansion + Optimal API

This commit is contained in:
Renn F
2025-12-27 21:53:38 +01:00
parent 3f48a93504
commit 1116e597d0
58 changed files with 11768 additions and 7840 deletions
+576
View File
@@ -0,0 +1,576 @@
# Code Review Guidelines
Standards for conducting effective code reviews in the RoboCo system.
---
## Table of Contents
1. [Review Philosophy](#review-philosophy)
2. [Reviewer Responsibilities](#reviewer-responsibilities)
3. [Author Responsibilities](#author-responsibilities)
4. [Review Checklist](#review-checklist)
5. [Feedback Guidelines](#feedback-guidelines)
6. [Severity Classification](#severity-classification)
7. [Common Issues](#common-issues)
8. [Automated Checks](#automated-checks)
---
## Review Philosophy
### CR-001: Purpose of Code Review
**Goals:**
1. **Catch bugs** - Find defects before they reach production
2. **Maintain quality** - Ensure code meets standards
3. **Share knowledge** - Spread understanding across team
4. **Improve design** - Identify better approaches
5. **Ensure consistency** - Keep codebase uniform
**NOT goals:**
- Demonstrate superiority
- Nitpick style (that's what linters are for)
- Rewrite someone's code
- Block progress indefinitely
### CR-002: Review Mindset
**As Reviewer:**
- Assume the author did their best
- Ask questions before making assumptions
- Explain the "why" behind suggestions
- Be constructive, not destructive
- Praise good work
**As Author:**
- Code review is about the code, not you
- Every suggestion is an opportunity to learn
- Explain your reasoning when disagreeing
- Thank reviewers for their time
---
## Reviewer Responsibilities
### CR-010: Response Time
| Priority | First Response | Full Review |
|----------|---------------|-------------|
| Urgent (blocker fix) | < 2 hours | < 4 hours |
| Normal | < 4 hours | < 1 day |
| Low (refactor, docs) | < 1 day | < 2 days |
### CR-011: Review Depth
**Quick Pass (5 min):**
- Does the PR description make sense?
- Are tests included?
- Does CI pass?
**Thorough Review (30+ min):**
- Understand the full context
- Check logic and edge cases
- Verify tests cover scenarios
- Review documentation updates
### CR-012: What to Review
| Must Review | Should Review | Don't Review |
|-------------|---------------|--------------|
| Logic correctness | Code style | Auto-generated code |
| Error handling | Performance | Formatting (linter handles) |
| Security concerns | Naming | Import order (linter handles) |
| Test coverage | Documentation | |
| API contracts | Code organization | |
---
## Author Responsibilities
### CR-020: Before Requesting Review
**Pre-submission Checklist:**
```markdown
- [ ] All automated checks pass (lint, type check, tests)
- [ ] Self-reviewed the diff
- [ ] PR description explains the change
- [ ] Tests cover new functionality
- [ ] Documentation updated if needed
- [ ] No debugging code left in
- [ ] No unrelated changes included
- [ ] Commit history is clean
```
### CR-021: Writing Good PR Descriptions
**Template:**
```markdown
## Summary
Brief description of what this PR does.
## Changes
- Added X functionality
- Modified Y behavior
- Removed deprecated Z
## Testing
How to test this change:
1. Step one
2. Step two
3. Expected result
## Related
- Task: TASK-123
- Related PR: #456
```
### CR-022: Keeping PRs Small
**Size Guidelines:**
| Lines Changed | Classification | Review Time |
|---------------|----------------|-------------|
| < 100 | Small | 15 min |
| 100-300 | Medium | 30 min |
| 300-500 | Large | 1 hour |
| > 500 | Too Large | Split it! |
**How to Split Large PRs:**
1. **By layer**: API → Service → Repository
2. **By feature**: Core logic → Edge cases → Polish
3. **By concern**: Main feature → Tests → Docs
---
## Review Checklist
### CR-030: Functionality
```markdown
- [ ] Code does what PR description says
- [ ] Edge cases are handled
- [ ] Error conditions are handled gracefully
- [ ] No obvious bugs or logic errors
- [ ] Performance is acceptable for use case
```
### CR-031: Code Quality
```markdown
- [ ] Follows project coding standards
- [ ] No code duplication (DRY)
- [ ] Functions/classes have single responsibility
- [ ] Naming is clear and consistent
- [ ] Comments explain "why", not "what"
- [ ] No dead code or TODOs without context
```
### CR-032: Security
```markdown
- [ ] No hardcoded secrets or credentials
- [ ] User input is validated and sanitized
- [ ] SQL queries use parameterized statements
- [ ] No command injection vulnerabilities
- [ ] Sensitive data is not logged
- [ ] Access control is properly enforced
```
### CR-033: Testing
```markdown
- [ ] New code has tests
- [ ] Tests cover happy path
- [ ] Tests cover error cases
- [ ] Tests are readable and maintainable
- [ ] No tests skipped without reason
- [ ] Mocking is appropriate (not excessive)
```
### CR-034: API Design
```markdown
- [ ] API is intuitive and consistent
- [ ] Breaking changes are noted
- [ ] Error responses are informative
- [ ] Documentation is updated
- [ ] Backwards compatibility maintained
```
### CR-035: Data Handling
```markdown
- [ ] Database migrations are reversible
- [ ] Indexes are used appropriately
- [ ] No N+1 query issues
- [ ] Large data sets are handled efficiently
- [ ] Transactions are used correctly
```
---
## Feedback Guidelines
### CR-040: How to Give Feedback
**Structure:**
```markdown
[Severity]: [Issue]
[Context/Reason]
[Suggestion if applicable]
```
**Examples:**
```markdown
# Good feedback
BLOCKER: This SQL query is vulnerable to injection
The user input is concatenated directly. An attacker could
extract all data with: `'; DROP TABLE users; --`
Suggestion: Use parameterized queries:
```python
await db.execute("SELECT * FROM users WHERE id = :id", {"id": user_id})
```
# Bad feedback
"This is wrong."
```
### CR-041: Severity Prefixes
Use prefixes to indicate urgency:
| Prefix | Meaning | Action |
|--------|---------|--------|
| `BLOCKER:` | Must fix, security/correctness issue | Cannot merge |
| `MAJOR:` | Should fix, significant concern | Should address |
| `MINOR:` | Nice to fix, improvement | Can defer |
| `NIT:` | Nitpick, style preference | Optional |
| `QUESTION:` | Need clarification | Explain |
| `PRAISE:` | Good work | Keep doing this! |
### CR-042: Types of Comments
**Actionable:**
```markdown
MAJOR: This function modifies its input parameter, which can cause
unexpected behavior for callers. Consider returning a new object instead.
```
**Question (non-blocking):**
```markdown
QUESTION: Is this timeout intentionally set to 5 minutes? Seems long
for a health check.
```
**Suggestion (optional):**
```markdown
NIT: Could use list comprehension here for readability:
`[x.name for x in items if x.active]`
```
**Praise:**
```markdown
PRAISE: Great error handling here! The retry logic with backoff
is exactly what we need for this external API.
```
### CR-043: What NOT to Do
**Avoid:**
- Personal attacks: "Who wrote this garbage?"
- Vague criticism: "This is confusing"
- Style debates: "I prefer X" (unless it violates standards)
- Demands without explanation: "Change this"
- Blocking for non-issues: Minor style preferences
---
## Severity Classification
### CR-050: Severity Definitions
| Severity | Definition | Examples |
|----------|------------|----------|
| **BLOCKER** | Security vulnerability, data loss risk, breaks build | SQL injection, missing auth check, crashes |
| **MAJOR** | Significant bug, performance issue, design flaw | Logic error, N+1 queries, missing validation |
| **MINOR** | Improvement opportunity, minor bug | Better naming, missing edge case, documentation |
| **NIT** | Style preference, optional enhancement | Alternative approach, formatting preference |
### CR-051: Blocking vs Non-Blocking
**Block merge for:**
- Security vulnerabilities (any severity)
- Logic errors that affect functionality
- Missing tests for critical paths
- Breaking API changes without migration
- Violations of ERROR-level coding standards
**Don't block merge for:**
- Style preferences covered by linters
- "I would have done it differently"
- Missing documentation (unless API change)
- Code that works but isn't "perfect"
- Minor optimizations
---
## Common Issues
### CR-060: Logic Issues
**Missing null checks:**
```python
# Bad
user.name.lower() # What if user is None?
# Good
if user and user.name:
user.name.lower()
```
**Off-by-one errors:**
```python
# Bad
for i in range(len(items) + 1): # IndexError on last iteration
items[i]
# Good
for i in range(len(items)):
items[i]
```
**Race conditions:**
```python
# Bad
if task.status == "pending":
# Another process could change status here!
task.status = "claimed"
db.save(task)
# Good - Use atomic operations
await db.execute(
"UPDATE tasks SET status = 'claimed' WHERE id = :id AND status = 'pending'",
{"id": task.id}
)
```
### CR-061: Performance Issues
**N+1 queries:**
```python
# Bad
tasks = await db.query(Task).all()
for task in tasks:
owner = await db.query(User).filter_by(id=task.owner_id).first() # N queries!
# Good
tasks = await db.query(Task).options(selectinload(Task.owner)).all()
```
**Unbounded queries:**
```python
# Bad
users = await db.query(User).all() # Could be millions
# Good
users = await db.query(User).limit(100).all()
```
**Memory bloat:**
```python
# Bad
data = [x for x in huge_iterator] # Loads all into memory
# Good
for x in huge_iterator: # Process one at a time
process(x)
```
### CR-062: Security Issues
**SQL injection:**
```python
# Bad
f"SELECT * FROM users WHERE id = '{user_input}'"
# Good
"SELECT * FROM users WHERE id = :id", {"id": user_input}
```
**Missing authorization:**
```python
# Bad
@router.delete("/tasks/{task_id}")
async def delete_task(task_id: str) -> None:
await db.delete_task(task_id) # Anyone can delete any task!
# Good
@router.delete("/tasks/{task_id}")
async def delete_task(task_id: str, current_user: User = Depends()) -> None:
task = await db.get_task(task_id)
if task.owner_id != current_user.id:
raise HTTPException(403, "Not authorized")
await db.delete_task(task_id)
```
**Sensitive data exposure:**
```python
# Bad
logger.info(f"User login: {user.email}, password: {password}")
# Good
logger.info("User login", user_id=user.id)
```
---
## Automated Checks
### CR-070: Required Checks
All PRs must pass these automated checks before merge:
| Check | Tool | Purpose |
|-------|------|---------|
| Formatting | ruff format | Code style consistency |
| Linting | ruff check | Code quality issues |
| Type checking | mypy | Type safety |
| Tests | pytest | Functionality verification |
| Dead code | vulture | Remove unused code |
| Security | bandit | Security vulnerabilities |
| Complexity | xenon | Maintainability |
### CR-071: CI Pipeline
```yaml
# .github/workflows/ci.yml
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- name: Format check
run: uv run ruff format --check .
- name: Lint
run: uv run ruff check .
- name: Type check
run: uv run mypy roboco/
- name: Tests
run: uv run pytest --cov=roboco --cov-fail-under=80
- name: Security scan
run: uv run bandit -r roboco/ -ll
```
### CR-072: Pre-commit Hooks
Use pre-commit hooks to catch issues early:
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ruff-format
name: ruff format
entry: uv run ruff format
language: system
types: [python]
- id: ruff-check
name: ruff check
entry: uv run ruff check --fix
language: system
types: [python]
- id: mypy
name: mypy
entry: uv run mypy
language: system
types: [python]
```
---
## Quick Reference
### Review Flow
```
1. Author creates PR
└─► Auto-checks run
2. Reviewer assigned
└─► Quick pass (5 min)
└─► Issues? Request changes early
3. Full review
└─► Check functionality
└─► Check code quality
└─► Check security
└─► Check tests
4. Feedback given
└─► BLOCKER/MAJOR: Must address
└─► MINOR/NIT: Optional
5. Author addresses feedback
└─► Push changes
└─► Reply to comments
6. Re-review if needed
└─► Approve or request more changes
7. Merge
└─► Delete branch
```
### Comment Templates
**Blocker:**
```markdown
BLOCKER: [Brief issue]
[Why this is a problem]
[How to fix it]
```
**Question:**
```markdown
QUESTION: [What you don't understand]
[Context for why you're asking]
```
**Praise:**
```markdown
PRAISE: [What's good about this]
[Why it's particularly good]
```
### Time Estimates
| PR Size | Lines | Review Time |
|---------|-------|-------------|
| XS | < 50 | 10 min |
| S | 50-100 | 15 min |
| M | 100-300 | 30 min |
| L | 300-500 | 1 hour |
| XL | > 500 | Split it! |
@@ -0,0 +1,997 @@
# Design Principles
Foundational design principles for building maintainable, scalable software in the RoboCo system.
---
## Table of Contents
1. [SOLID Principles](#solid-principles)
2. [DRY - Don't Repeat Yourself](#dry---dont-repeat-yourself)
3. [KISS - Keep It Simple, Stupid](#kiss---keep-it-simple-stupid)
4. [YAGNI - You Aren't Gonna Need It](#yagni---you-arent-gonna-need-it)
5. [Separation of Concerns](#separation-of-concerns)
6. [Composition Over Inheritance](#composition-over-inheritance)
7. [Fail Fast](#fail-fast)
8. [Law of Demeter](#law-of-demeter)
9. [Dependency Injection](#dependency-injection)
10. [Immutability](#immutability)
---
## SOLID Principles
### ARCH-001: Single Responsibility Principle (SRP)
**Severity:** WARNING
**Principle:** A class should have one, and only one, reason to change.
Each module, class, or function should do one thing well.
```python
# Bad - Multiple responsibilities
class TaskManager:
def create_task(self, data: TaskCreate) -> Task:
# Creates task
...
def send_notification(self, task: Task, recipient: str) -> None:
# Sends notification
...
def generate_report(self, tasks: list[Task]) -> Report:
# Generates report
...
# Good - Single responsibility per class
class TaskService:
def create(self, data: TaskCreate) -> Task:
...
class NotificationService:
def send(self, notification: Notification) -> None:
...
class ReportService:
def generate(self, tasks: list[Task]) -> Report:
...
```
### ARCH-002: Open/Closed Principle (OCP)
**Severity:** WARNING
**Principle:** Software entities should be open for extension, but closed for modification.
Design systems that can be extended without modifying existing code.
```python
# Bad - Need to modify class for new types
class TaskProcessor:
def process(self, task: Task) -> None:
if task.type == "bug":
self._process_bug(task)
elif task.type == "feature":
self._process_feature(task)
elif task.type == "refactor": # Added later - modifies existing code
self._process_refactor(task)
# Good - Extend via new classes
from abc import ABC, abstractmethod
class TaskProcessor(ABC):
@abstractmethod
def process(self, task: Task) -> None:
...
class BugProcessor(TaskProcessor):
def process(self, task: Task) -> None:
...
class FeatureProcessor(TaskProcessor):
def process(self, task: Task) -> None:
...
class RefactorProcessor(TaskProcessor): # Added without modifying existing code
def process(self, task: Task) -> None:
...
# Registry pattern for extension
PROCESSORS: dict[str, type[TaskProcessor]] = {
"bug": BugProcessor,
"feature": FeatureProcessor,
"refactor": RefactorProcessor,
}
def get_processor(task_type: str) -> TaskProcessor:
return PROCESSORS[task_type]()
```
### ARCH-003: Liskov Substitution Principle (LSP)
**Severity:** ERROR
**Principle:** Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
Derived classes must be substitutable for their base classes.
```python
# Bad - Subclass violates base class contract
class Bird:
def fly(self) -> None:
print("Flying")
class Penguin(Bird):
def fly(self) -> None:
raise NotImplementedError("Penguins can't fly!") # Violates LSP
# Good - Proper abstraction
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self) -> None:
...
class FlyingBird(Bird):
def move(self) -> None:
self.fly()
def fly(self) -> None:
print("Flying")
class SwimmingBird(Bird):
def move(self) -> None:
self.swim()
def swim(self) -> None:
print("Swimming")
```
### ARCH-004: Interface Segregation Principle (ISP)
**Severity:** WARNING
**Principle:** Many client-specific interfaces are better than one general-purpose interface.
Don't force clients to depend on methods they don't use.
```python
# Bad - Fat interface
class Worker(ABC):
@abstractmethod
def work(self) -> None: ...
@abstractmethod
def eat(self) -> None: ...
@abstractmethod
def sleep(self) -> None: ...
class Robot(Worker):
def work(self) -> None:
...
def eat(self) -> None:
raise NotImplementedError() # Robots don't eat
def sleep(self) -> None:
raise NotImplementedError() # Robots don't sleep
# Good - Segregated interfaces
class Workable(ABC):
@abstractmethod
def work(self) -> None: ...
class Feedable(ABC):
@abstractmethod
def eat(self) -> None: ...
class Sleepable(ABC):
@abstractmethod
def sleep(self) -> None: ...
class Human(Workable, Feedable, Sleepable):
def work(self) -> None: ...
def eat(self) -> None: ...
def sleep(self) -> None: ...
class Robot(Workable):
def work(self) -> None: ...
```
### ARCH-005: Dependency Inversion Principle (DIP)
**Severity:** ERROR
**Principle:** Depend on abstractions, not concretions.
High-level modules should not depend on low-level modules.
```python
# Bad - High-level depends on low-level
class PostgreSQLDatabase:
def query(self, sql: str) -> list[dict]:
...
class TaskRepository:
def __init__(self) -> None:
self.db = PostgreSQLDatabase() # Tight coupling
def get_task(self, task_id: str) -> Task:
return self.db.query(f"SELECT * FROM tasks WHERE id = '{task_id}'")
# Good - Depend on abstraction
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
async def query(self, sql: str, params: dict) -> list[dict]:
...
class PostgreSQLDatabase(Database):
async def query(self, sql: str, params: dict) -> list[dict]:
...
class TaskRepository:
def __init__(self, db: Database) -> None:
self.db = db # Depends on abstraction
async def get_task(self, task_id: str) -> Task:
result = await self.db.query(
"SELECT * FROM tasks WHERE id = :id",
{"id": task_id}
)
return Task(**result[0])
```
---
## DRY - Don't Repeat Yourself
### ARCH-010: No Code Duplication
**Severity:** WARNING
**Principle:** Every piece of knowledge must have a single, unambiguous, authoritative representation.
Eliminate duplication of logic, data, and knowledge.
```python
# Bad - Duplicated validation logic
class UserService:
def create_user(self, email: str) -> User:
if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
raise ValueError("Invalid email")
...
class InviteService:
def send_invite(self, email: str) -> None:
if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email): # Duplicated!
raise ValueError("Invalid email")
...
# Good - Single source of truth
EMAIL_PATTERN = re.compile(r"^[\w\.-]+@[\w\.-]+\.\w+$")
def validate_email(email: str) -> str:
"""Validate and return email or raise ValueError."""
if not EMAIL_PATTERN.match(email):
raise ValueError(f"Invalid email: {email}")
return email.lower()
class UserService:
def create_user(self, email: str) -> User:
validated_email = validate_email(email)
...
class InviteService:
def send_invite(self, email: str) -> None:
validated_email = validate_email(email)
...
```
### ARCH-011: Extract Common Patterns
**Severity:** WARNING
When you see the same pattern three times, extract it.
```python
# Bad - Repeated error handling pattern
async def get_user(user_id: str) -> User:
try:
result = await db.query("SELECT * FROM users WHERE id = :id", {"id": user_id})
if not result:
raise NotFoundError(f"User {user_id} not found")
return User(**result[0])
except DatabaseError as e:
logger.error("Database error", error=str(e))
raise
async def get_task(task_id: str) -> Task:
try:
result = await db.query("SELECT * FROM tasks WHERE id = :id", {"id": task_id})
if not result:
raise NotFoundError(f"Task {task_id} not found")
return Task(**result[0])
except DatabaseError as e:
logger.error("Database error", error=str(e))
raise
# Good - Extract common pattern
T = TypeVar('T')
async def get_by_id(
table: str,
id_value: str,
model: type[T],
entity_name: str,
) -> T:
"""Generic get-by-id with error handling."""
try:
result = await db.query(
f"SELECT * FROM {table} WHERE id = :id",
{"id": id_value}
)
if not result:
raise NotFoundError(f"{entity_name} {id_value} not found")
return model(**result[0])
except DatabaseError as e:
logger.error("Database error", table=table, error=str(e))
raise
async def get_user(user_id: str) -> User:
return await get_by_id("users", user_id, User, "User")
async def get_task(task_id: str) -> Task:
return await get_by_id("tasks", task_id, Task, "Task")
```
### ARCH-012: But Avoid False DRY
**Severity:** INFO
Not all similar code is duplicate. Don't abstract too early.
```python
# False DRY - These look similar but serve different purposes
def format_user_name(user: User) -> str:
return f"{user.first_name} {user.last_name}"
def format_agent_name(agent: Agent) -> str:
return f"{agent.role}: {agent.slug}"
# Don't force these into a single function just because they both "format names"
# They have different semantics and will evolve independently
```
---
## KISS - Keep It Simple, Stupid
### ARCH-020: Prefer Simple Solutions
**Severity:** WARNING
**Principle:** The simplest solution that works is often the best.
Avoid unnecessary complexity.
```python
# Bad - Over-engineered solution
class TaskStatusStrategyFactory:
_strategies: dict[str, type[TaskStatusStrategy]] = {}
@classmethod
def register(cls, status: str) -> Callable:
def decorator(strategy_class: type[TaskStatusStrategy]) -> type[TaskStatusStrategy]:
cls._strategies[status] = strategy_class
return strategy_class
return decorator
@classmethod
def create(cls, task: Task) -> TaskStatusStrategy:
return cls._strategies[task.status]()
@TaskStatusStrategyFactory.register("pending")
class PendingStatusStrategy(TaskStatusStrategy):
def can_transition_to(self, new_status: str) -> bool:
return new_status in ["claimed", "cancelled"]
# Good - Simple and clear
VALID_TRANSITIONS = {
"pending": {"claimed", "cancelled"},
"claimed": {"in_progress", "pending"},
"in_progress": {"completed", "blocked", "paused"},
# ... etc
}
def can_transition(current: str, new: str) -> bool:
return new in VALID_TRANSITIONS.get(current, set())
```
### ARCH-021: Avoid Premature Abstraction
**Severity:** WARNING
Don't abstract before you have concrete requirements.
```python
# Bad - Premature abstraction
class AbstractDataProcessor(ABC):
@abstractmethod
def preprocess(self, data: Any) -> Any: ...
@abstractmethod
def process(self, data: Any) -> Any: ...
@abstractmethod
def postprocess(self, data: Any) -> Any: ...
def run(self, data: Any) -> Any:
data = self.preprocess(data)
data = self.process(data)
return self.postprocess(data)
# When you only have one implementation!
class TaskDataProcessor(AbstractDataProcessor):
def preprocess(self, data: Any) -> Any:
return data # Does nothing
def process(self, data: Any) -> Any:
return transform_task(data)
def postprocess(self, data: Any) -> Any:
return data # Does nothing
# Good - Start simple, abstract when needed
def process_task_data(data: dict) -> Task:
return transform_task(data)
# Later, when you ACTUALLY need abstraction:
# Then create the base class with proven patterns
```
### ARCH-022: Readable Over Clever
**Severity:** WARNING
Code is read more often than written. Optimize for readability.
```python
# Bad - Clever but unreadable
result = reduce(
lambda acc, x: {**acc, x[0]: x[1]},
filter(lambda t: t[1] > 0, map(lambda k: (k, data.get(k, 0)), keys)),
{}
)
# Good - Clear and readable
result = {}
for key in keys:
value = data.get(key, 0)
if value > 0:
result[key] = value
```
---
## YAGNI - You Aren't Gonna Need It
### ARCH-030: Don't Build Speculatively
**Severity:** WARNING
**Principle:** Only implement features when you actually need them.
```python
# Bad - Building for hypothetical future
class TaskService:
def __init__(
self,
db: Database,
cache: Cache,
queue: MessageQueue,
analytics: AnalyticsService,
audit_log: AuditLogService,
rate_limiter: RateLimiter,
circuit_breaker: CircuitBreaker,
feature_flags: FeatureFlagService,
) -> None:
# Most of these aren't used yet
...
def create_task(self, data: TaskCreate) -> Task:
# Just creates a task in the database
return self.db.create_task(data)
# Good - Only what you need now
class TaskService:
def __init__(self, db: Database) -> None:
self.db = db
async def create_task(self, data: TaskCreate) -> Task:
return await self.db.create_task(data)
# Add cache, queue, etc. when you actually need them
```
### ARCH-031: Delete Unused Code
**Severity:** ERROR
**Tools:** vulture
Remove dead code. It's not "just in case" - it's noise.
```python
# Bad - Keeping "just in case" code
class TaskService:
def create_task(self, data: TaskCreate) -> Task:
...
# def create_task_v2(self, data: TaskCreateV2) -> Task:
# """New version - not sure if we'll use this"""
# ...
# def _experimental_feature(self) -> None:
# """Might need this later"""
# pass
# Good - Clean codebase
class TaskService:
def create_task(self, data: TaskCreate) -> Task:
...
# Use version control for history, not comments
```
---
## Separation of Concerns
### ARCH-040: Layer Architecture
**Severity:** ERROR
Organize code into distinct layers with clear responsibilities.
```
┌─────────────────────────────────────────┐
│ API Layer (Routes) │ ← HTTP handling, validation
├─────────────────────────────────────────┤
│ Service Layer (Business) │ ← Business logic, orchestration
├─────────────────────────────────────────┤
│ Repository Layer (Data Access) │ ← Database queries, caching
├─────────────────────────────────────────┤
│ Model Layer (Domain) │ ← Data structures, entities
└─────────────────────────────────────────┘
```
```python
# Good - Clear layer separation
# models/task.py - Domain entities
class Task(BaseModel):
id: UUID
title: str
status: TaskStatus
# repositories/task.py - Data access
class TaskRepository:
async def get_by_id(self, task_id: UUID) -> Task | None:
result = await self.db.query(...)
return Task(**result) if result else None
# services/task.py - Business logic
class TaskService:
def __init__(self, repo: TaskRepository, notifier: NotificationService) -> None:
self.repo = repo
self.notifier = notifier
async def complete_task(self, task_id: UUID) -> Task:
task = await self.repo.get_by_id(task_id)
if task is None:
raise TaskNotFoundError(task_id)
task.status = TaskStatus.COMPLETED
await self.repo.update(task)
await self.notifier.notify_completion(task)
return task
# api/routes/tasks.py - HTTP handling
@router.post("/tasks/{task_id}/complete")
async def complete_task(task_id: UUID, service: TaskService = Depends()) -> TaskResponse:
task = await service.complete_task(task_id)
return TaskResponse.from_orm(task)
```
### ARCH-041: No Business Logic in Routes
**Severity:** ERROR
API routes should only handle HTTP concerns.
```python
# Bad - Business logic in route
@router.post("/tasks")
async def create_task(request: TaskCreate, db: AsyncSession = Depends()) -> TaskResponse:
# Validation
if len(request.title) < 3:
raise HTTPException(400, "Title too short")
# Business logic (should be in service)
task = Task(**request.dict())
task.created_at = datetime.now(UTC)
task.status = TaskStatus.PENDING
if request.assigned_to:
agent = await db.query(Agent).filter_by(id=request.assigned_to).first()
if agent is None:
raise HTTPException(400, "Agent not found")
task.assigned_to = agent.id
db.add(task)
await db.commit()
# Send notification (should be in service)
await send_notification(task.assigned_to, f"New task: {task.title}")
return TaskResponse.from_orm(task)
# Good - Route delegates to service
@router.post("/tasks")
async def create_task(
request: TaskCreate,
service: TaskService = Depends(),
) -> TaskResponse:
task = await service.create(request)
return TaskResponse.from_orm(task)
```
### ARCH-042: No Database Queries in Routes
**Severity:** ERROR
Database access should be in repository or service layer.
```python
# Bad - Direct DB access in route
@router.get("/tasks")
async def list_tasks(
status: TaskStatus | None = None,
db: AsyncSession = Depends(),
) -> list[TaskResponse]:
query = select(Task)
if status:
query = query.where(Task.status == status)
result = await db.execute(query)
return [TaskResponse.from_orm(t) for t in result.scalars()]
# Good - Delegate to service/repository
@router.get("/tasks")
async def list_tasks(
status: TaskStatus | None = None,
service: TaskService = Depends(),
) -> list[TaskResponse]:
tasks = await service.list(status=status)
return [TaskResponse.from_orm(t) for t in tasks]
```
---
## Composition Over Inheritance
### ARCH-050: Prefer Composition
**Severity:** WARNING
**Principle:** Favor object composition over class inheritance.
```python
# Bad - Deep inheritance hierarchy
class BaseProcessor:
def process(self, data: Any) -> Any:
...
class ValidatingProcessor(BaseProcessor):
def process(self, data: Any) -> Any:
self.validate(data)
return super().process(data)
class LoggingValidatingProcessor(ValidatingProcessor):
def process(self, data: Any) -> Any:
self.log_start(data)
result = super().process(data)
self.log_end(result)
return result
class CachingLoggingValidatingProcessor(LoggingValidatingProcessor):
... # Getting ridiculous
# Good - Composition with mixins or decorators
class TaskProcessor:
def __init__(
self,
validator: Validator | None = None,
logger: Logger | None = None,
cache: Cache | None = None,
) -> None:
self.validator = validator
self.logger = logger
self.cache = cache
def process(self, data: Any) -> Any:
if self.validator:
self.validator.validate(data)
if self.logger:
self.logger.log_start(data)
result = self._do_process(data)
if self.cache:
self.cache.set(data.id, result)
if self.logger:
self.logger.log_end(result)
return result
```
### ARCH-051: Use Protocols Over ABC
**Severity:** INFO
**Python:** Use Protocol for structural typing when possible.
```python
# Good - Protocol-based typing
from typing import Protocol
class Repository(Protocol):
async def get(self, id: str) -> dict | None: ...
async def save(self, entity: dict) -> None: ...
class TaskService:
def __init__(self, repo: Repository) -> None:
self.repo = repo
# Any class with get/save methods works, no inheritance needed
class InMemoryRepo:
async def get(self, id: str) -> dict | None:
return self.data.get(id)
async def save(self, entity: dict) -> None:
self.data[entity["id"]] = entity
# Works with TaskService without explicit inheritance!
```
---
## Fail Fast
### ARCH-060: Validate Early
**Severity:** ERROR
**Principle:** Detect and report errors as early as possible.
```python
# Bad - Late failure
def process_order(order: dict) -> Receipt:
# ... lots of processing ...
# Fails late after doing work
if order.get("customer_id") is None:
raise ValueError("Missing customer_id")
# More processing that depends on customer_id
...
# Good - Fail fast
def process_order(order: dict) -> Receipt:
# Validate immediately
if order.get("customer_id") is None:
raise ValueError("Missing customer_id")
if order.get("items") is None or len(order["items"]) == 0:
raise ValueError("Order must have items")
# Now process with confidence
...
```
### ARCH-061: Use Guard Clauses
**Severity:** WARNING
Return early for invalid states instead of deep nesting.
```python
# Bad - Deep nesting
def process_task(task: Task | None, agent: Agent | None) -> Result:
if task is not None:
if task.status == TaskStatus.PENDING:
if agent is not None:
if agent.can_claim(task):
return do_process(task, agent)
else:
return Result(error="Agent cannot claim")
else:
return Result(error="No agent")
else:
return Result(error="Task not pending")
else:
return Result(error="No task")
# Good - Guard clauses
def process_task(task: Task | None, agent: Agent | None) -> Result:
if task is None:
return Result(error="No task")
if task.status != TaskStatus.PENDING:
return Result(error="Task not pending")
if agent is None:
return Result(error="No agent")
if not agent.can_claim(task):
return Result(error="Agent cannot claim")
return do_process(task, agent)
```
---
## Law of Demeter
### ARCH-070: Don't Talk to Strangers
**Severity:** WARNING
**Principle:** Only talk to immediate friends, not friends of friends.
```python
# Bad - Chained method calls
def get_customer_city(order: Order) -> str:
return order.customer.address.city.name
# If any of these are None, it fails
# Also tightly coupled to internal structure
# Good - Ask, don't dig
class Order:
def get_customer_city(self) -> str:
return self.customer.get_city_name()
class Customer:
def get_city_name(self) -> str:
if self.address and self.address.city:
return self.address.city.name
return "Unknown"
```
---
## Dependency Injection
### ARCH-080: Inject Dependencies
**Severity:** ERROR
**Principle:** Dependencies should be provided, not created internally.
```python
# Bad - Creates own dependencies
class TaskService:
def __init__(self) -> None:
self.db = PostgresDatabase() # Hard to test
self.cache = RedisCache() # Hard to swap
self.notifier = EmailNotifier()
# Good - Inject dependencies
class TaskService:
def __init__(
self,
db: Database,
cache: Cache,
notifier: Notifier,
) -> None:
self.db = db
self.cache = cache
self.notifier = notifier
# FastAPI dependency injection
def get_task_service(
db: Database = Depends(get_database),
cache: Cache = Depends(get_cache),
notifier: Notifier = Depends(get_notifier),
) -> TaskService:
return TaskService(db, cache, notifier)
```
---
## Immutability
### ARCH-090: Prefer Immutable Data
**Severity:** WARNING
**Principle:** Immutable objects are easier to reason about and safer in concurrent code.
```python
# Bad - Mutable state
class Task:
def __init__(self, title: str) -> None:
self.title = title
self.tags = [] # Mutable!
task = Task("Fix bug")
task.tags.append("urgent")
task.title = "Changed!" # Mutation
# Good - Immutable with dataclasses
from dataclasses import dataclass
@dataclass(frozen=True)
class Task:
title: str
tags: tuple[str, ...] = ()
def with_tag(self, tag: str) -> "Task":
"""Return new Task with additional tag."""
return Task(
title=self.title,
tags=self.tags + (tag,)
)
task = Task("Fix bug")
task_with_tag = task.with_tag("urgent") # Returns new instance
```
### ARCH-091: Avoid Side Effects in Functions
**Severity:** WARNING
Pure functions are easier to test and reason about.
```python
# Bad - Side effects
def process_tasks(tasks: list[Task]) -> None:
for task in tasks:
task.processed = True # Mutates input!
global_counter += 1 # Global state!
send_notification() # Side effect!
# Good - Pure function
def process_tasks(tasks: list[Task]) -> list[ProcessedTask]:
return [
ProcessedTask(
task=task,
processed_at=datetime.now(UTC)
)
for task in tasks
]
# Handle side effects separately
processed = process_tasks(tasks)
for p in processed:
await notifier.send(p)
```
---
## Quick Reference
### Principle Severity
| Principle | Severity | Impact |
|-----------|----------|--------|
| Liskov Substitution | ERROR | Breaks polymorphism |
| Dependency Inversion | ERROR | Prevents testing |
| No DB in Routes | ERROR | Violates layering |
| Validate Early | ERROR | Wastes resources |
| Inject Dependencies | ERROR | Untestable code |
| Single Responsibility | WARNING | Maintenance burden |
| Open/Closed | WARNING | Modification risk |
| DRY | WARNING | Bug propagation |
| KISS | WARNING | Complexity cost |
| YAGNI | WARNING | Wasted effort |
| Composition | WARNING | Rigid hierarchy |
| Immutability | WARNING | Concurrency bugs |
### Anti-Pattern Detection
| Anti-Pattern | Signs | Fix |
|--------------|-------|-----|
| God Class | Class > 500 lines | Split by responsibility |
| Feature Envy | Uses other class's data heavily | Move method to that class |
| Shotgun Surgery | One change requires many edits | Consolidate related code |
| Primitive Obsession | Many primitives for one concept | Create value object |
| Long Parameter List | > 5 parameters | Use parameter object |
| Data Clumps | Same data together often | Create class for data |
| Speculative Generality | "We might need this" | Delete until needed |
+923
View File
@@ -0,0 +1,923 @@
# Python Coding Standards
Comprehensive standards for Python development in the RoboCo system. These standards are enforced through automated tooling (see [Tooling Enforcement](#tooling-enforcement) section).
---
## Table of Contents
1. [Code Style](#code-style)
2. [Type Safety](#type-safety)
3. [Error Handling](#error-handling)
4. [Data Validation](#data-validation)
5. [Async Patterns](#async-patterns)
6. [Testing](#testing)
7. [Dependencies](#dependencies)
8. [Tooling Enforcement](#tooling-enforcement)
9. [Code Complexity](#code-complexity)
10. [Security](#security)
---
## Code Style
### PY-001: Use Type Hints
**Severity:** ERROR
**Tools:** mypy, ruff
All function signatures MUST include type hints for parameters and return values.
```python
# Good
def process_task(task_id: str, priority: int = 1) -> TaskResult:
...
async def fetch_user(user_id: UUID) -> User | None:
...
# Bad - Missing type hints
def process_task(task_id, priority=1):
...
```
### PY-002: Docstrings Required
**Severity:** WARNING
**Tools:** ruff (D100-D417)
All public functions, classes, and modules MUST have docstrings following Google style.
```python
def calculate_metrics(data: list[float]) -> MetricsResult:
"""Calculate statistical metrics from data points.
Args:
data: List of numeric values to analyze.
Returns:
MetricsResult containing mean, median, and std dev.
Raises:
ValueError: If data is empty.
"""
```
### PY-003: Import Organization
**Severity:** ERROR
**Tools:** ruff (I001-I002)
Imports MUST be sorted in this order: stdlib, third-party, local. Use `ruff` to enforce.
```python
# Good
import asyncio
from pathlib import Path
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from roboco.models import Task, User
from roboco.services import TaskService
```
### PY-004: Line Length
**Severity:** ERROR
**Tools:** ruff (E501)
Maximum line length is 88 characters (Black default). Use line breaks for long expressions.
```python
# Good - Multi-line function call
result = await service.create_task(
title=request.title,
description=request.description,
assigned_to=agent_id,
priority=request.priority,
)
# Good - Multi-line string
error_message = (
f"Task {task_id} cannot be claimed: "
f"current status is {task.status}, expected 'pending'"
)
```
### PY-005: Naming Conventions
**Severity:** ERROR
**Tools:** ruff (N801-N818)
| Type | Convention | Example |
|------|------------|---------|
| Classes | PascalCase | `TaskService`, `UserModel` |
| Functions | snake_case | `get_user`, `process_task` |
| Variables | snake_case | `user_id`, `task_count` |
| Constants | SCREAMING_SNAKE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
| Private | Leading underscore | `_internal_method`, `_cache` |
| Type Variables | PascalCase | `T`, `TaskT`, `ResponseT` |
---
## Type Safety
### PY-010: Strict Mypy Configuration
**Severity:** ERROR
**Tools:** mypy
The project uses strict mypy configuration. All code MUST pass these checks:
```toml
# pyproject.toml settings (enforced)
[tool.mypy]
python_version = "3.13"
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
strict_optional = true
warn_return_any = true
warn_unreachable = true
```
### PY-011: No `Any` Types
**Severity:** ERROR
**Tools:** mypy
Avoid `Any` type. Use `object`, generics, or `TypeVar` instead.
```python
# Bad
def process_data(data: Any) -> Any:
...
# Good - Use generics
T = TypeVar('T')
def process_data(data: T) -> T:
...
# Good - Use Union for multiple types
def process_data(data: str | bytes) -> ProcessedData:
...
```
### PY-012: Use `None` Explicitly
**Severity:** ERROR
**Tools:** mypy
Use `| None` for optional values. Never use implicit optional.
```python
# Bad - Implicit optional
def get_user(user_id: str, cache: dict = None) -> User:
...
# Good - Explicit optional
def get_user(user_id: str, cache: dict[str, User] | None = None) -> User:
...
```
### PY-013: Use Type Aliases for Complex Types
**Severity:** WARNING
**Tools:** ruff
Create type aliases for complex or repeated types.
```python
# Good - Type aliases improve readability
type TaskCallback = Callable[[Task, TaskStatus], Awaitable[None]]
type SearchResults = list[tuple[str, float, dict[str, Any]]]
async def search_with_callback(
query: str,
callback: TaskCallback,
) -> SearchResults:
...
```
---
## Error Handling
### PY-020: Specific Exceptions
**Severity:** ERROR
**Tools:** ruff (E722, B001)
NEVER use bare `except:`. Catch specific exceptions.
```python
# Good
try:
result = await service.process(data)
except ValidationError as e:
logger.warning("Validation failed", error=str(e))
raise
except ServiceUnavailableError:
await retry_with_backoff(service.process, data)
# Bad - NEVER do this
try:
result = await service.process(data)
except:
pass
```
### PY-021: Use Custom Exceptions
**Severity:** WARNING
**Tools:** code review
Define domain-specific exceptions for better error handling.
```python
# Good - Custom exceptions
class TaskError(Exception):
"""Base exception for task operations."""
class TaskNotFoundError(TaskError):
"""Task does not exist."""
class TaskAlreadyClaimedError(TaskError):
"""Task is already claimed by another agent."""
# Usage
async def claim_task(task_id: str, agent_id: str) -> Task:
task = await get_task(task_id)
if task is None:
raise TaskNotFoundError(f"Task {task_id} not found")
if task.assigned_to and task.assigned_to != agent_id:
raise TaskAlreadyClaimedError(f"Task claimed by {task.assigned_to}")
...
```
### PY-022: Structured Logging
**Severity:** ERROR
**Tools:** ruff, code review
Use structlog with context for all logging. NEVER use print statements.
```python
# Good
import structlog
logger = structlog.get_logger(__name__)
logger.info(
"Task completed",
task_id=task.id,
duration_ms=elapsed,
agent_id=agent.id,
)
# Bad - Never use print
print(f"Task {task.id} completed in {elapsed}ms")
```
### PY-023: Re-raise with Context
**Severity:** WARNING
**Tools:** code review
When catching and re-raising, preserve the original exception chain.
```python
# Good - Preserve exception chain
try:
result = await external_api.call()
except ExternalAPIError as e:
raise ServiceError("External API failed") from e
# Bad - Loses original traceback
try:
result = await external_api.call()
except ExternalAPIError:
raise ServiceError("External API failed")
```
---
## Data Validation
### PY-030: Pydantic Models
**Severity:** ERROR
**Tools:** ruff, mypy
Use Pydantic for all API request/response models and configuration.
```python
from pydantic import BaseModel, Field, field_validator
class TaskRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
priority: int = Field(default=1, ge=1, le=5)
tags: list[str] = Field(default_factory=list)
@field_validator('tags')
@classmethod
def validate_tags(cls, v: list[str]) -> list[str]:
return [tag.lower().strip() for tag in v]
```
### PY-031: Validate at Boundaries
**Severity:** ERROR
**Tools:** code review
Validate external input at system boundaries. Trust internal data.
```python
# API boundary - validate thoroughly
@router.post("/tasks")
async def create_task(request: TaskCreate) -> TaskResponse:
# Pydantic validates automatically
...
# Internal service - trust validated data
async def process_task(task: Task) -> None:
# No need to re-validate task.title here
...
```
### PY-032: Use Enums for Finite Values
**Severity:** WARNING
**Tools:** ruff
Use Enums for status values, types, and other finite sets.
```python
from enum import StrEnum
class TaskStatus(StrEnum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
# Usage - type-safe comparisons
if task.status == TaskStatus.PENDING:
...
```
---
## Async Patterns
### PY-040: Async by Default
**Severity:** ERROR
**Tools:** code review
Use async functions for ALL I/O operations. All database and API calls MUST be async.
```python
# Good - async I/O
async def fetch_user(user_id: str) -> User:
return await db.users.get(user_id)
async def call_external_api(data: dict) -> Response:
async with httpx.AsyncClient() as client:
return await client.post(url, json=data)
# Bad - Blocking I/O
def fetch_user(user_id: str) -> User:
return db.users.get(user_id) # Blocking!
```
### PY-041: Use `asyncio.gather` for Concurrent Operations
**Severity:** WARNING
**Tools:** code review
Execute independent async operations concurrently.
```python
# Good - Concurrent execution
async def get_task_details(task_id: str) -> TaskDetails:
task, comments, history = await asyncio.gather(
get_task(task_id),
get_comments(task_id),
get_history(task_id),
)
return TaskDetails(task=task, comments=comments, history=history)
# Bad - Sequential when not needed
async def get_task_details(task_id: str) -> TaskDetails:
task = await get_task(task_id)
comments = await get_comments(task_id) # Waits unnecessarily
history = await get_history(task_id) # Waits unnecessarily
...
```
### PY-042: Proper Context Manager Usage
**Severity:** ERROR
**Tools:** ruff (ASYNC)
Use async context managers for resources that need cleanup.
```python
# Good - Proper cleanup
async with AsyncSession(engine) as session:
async with session.begin():
result = await session.execute(query)
# Good - httpx client
async with httpx.AsyncClient() as client:
response = await client.get(url)
```
### PY-043: Avoid Blocking in Async Code
**Severity:** ERROR
**Tools:** ruff (ASYNC), bandit
NEVER call blocking functions from async code.
```python
# Bad - Blocks event loop
async def process_file(path: Path) -> str:
return path.read_text() # Blocking!
# Good - Use async file I/O
import aiofiles
async def process_file(path: Path) -> str:
async with aiofiles.open(path) as f:
return await f.read()
# Good - Run blocking in thread pool
async def process_file(path: Path) -> str:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, path.read_text)
```
---
## Testing
### PY-050: Test Coverage
**Severity:** ERROR
**Tools:** pytest-cov
Maintain minimum 80% code coverage for all modules.
```bash
# Run with coverage
uv run pytest --cov=roboco --cov-report=term-missing
```
### PY-051: Async Tests
**Severity:** ERROR
**Tools:** pytest-asyncio
Use pytest-asyncio for testing async code.
```python
import pytest
@pytest.mark.asyncio
async def test_fetch_user() -> None:
user = await fetch_user("test-123")
assert user.name == "Test User"
```
### PY-052: Test Structure
**Severity:** WARNING
**Tools:** code review
Follow AAA pattern: Arrange, Act, Assert.
```python
@pytest.mark.asyncio
async def test_task_claim_success() -> None:
# Arrange
task = await create_test_task(status=TaskStatus.PENDING)
agent = await create_test_agent()
# Act
claimed_task = await task_service.claim(task.id, agent.id)
# Assert
assert claimed_task.status == TaskStatus.CLAIMED
assert claimed_task.assigned_to == agent.id
```
### PY-053: Use Factories for Test Data
**Severity:** WARNING
**Tools:** code review
Use factory-boy for consistent test data generation.
```python
from factory import Factory, Faker, LazyAttribute
class TaskFactory(Factory):
class Meta:
model = Task
title = Faker('sentence')
status = TaskStatus.PENDING
created_at = LazyAttribute(lambda _: datetime.now(UTC))
```
---
## Dependencies
### PY-060: Use UV
**Severity:** ERROR
**Tools:** pyproject.toml
Use `uv` as the package manager. Lock dependencies in `pyproject.toml` and `uv.lock`.
```bash
# Add dependency
uv add package-name
# Add dev dependency
uv add --dev package-name
# Sync dependencies
uv sync
```
### PY-061: Pin Dependencies
**Severity:** WARNING
**Tools:** deptry
Keep `uv.lock` committed. Run `uv lock` when updating dependencies.
### PY-062: Audit Dependencies
**Severity:** ERROR
**Tools:** pip-audit, safety
Run security audits on dependencies regularly.
```bash
# Audit for vulnerabilities
uv run pip-audit
uv run safety scan
```
---
## Tooling Enforcement
All Python code MUST pass these automated checks before merge.
### Ruff (Linting & Formatting)
```bash
# Format code
uv run ruff format .
# Check linting
uv run ruff check .
# Auto-fix issues
uv run ruff check --fix .
```
**Enabled Rule Sets:**
| Rule | Description |
|------|-------------|
| `E`, `W` | pycodestyle (PEP 8) |
| `F` | Pyflakes (errors) |
| `I` | isort (imports) |
| `B` | flake8-bugbear (common bugs) |
| `C4` | flake8-comprehensions |
| `UP` | pyupgrade (Python upgrades) |
| `ARG` | unused arguments |
| `SIM` | simplification |
| `TCH` | type checking |
| `PTH` | pathlib usage |
| `PL` | Pylint |
| `RUF` | Ruff-specific |
### Mypy (Type Checking)
```bash
uv run mypy roboco/
```
**Configuration (pyproject.toml):**
```toml
[tool.mypy]
python_version = "3.13"
strict = true
plugins = ["pydantic.mypy"]
```
### Vulture (Dead Code)
```bash
uv run vulture roboco/ vulture_whitelist.py
```
Finds unused code. Add false positives to `vulture_whitelist.py`.
### Bandit (Security)
```bash
uv run bandit -r roboco/ -ll
```
Scans for security issues. Severity threshold: medium.
### Radon (Complexity)
```bash
# Cyclomatic complexity
uv run radon cc roboco/ -nc
# Maintainability index
uv run radon mi roboco/ -nc
```
### Xenon (Complexity Thresholds)
```bash
uv run xenon roboco/ --max-absolute B --max-modules A --max-average A
```
**Thresholds:**
| Metric | Maximum | Grade |
|--------|---------|-------|
| Absolute complexity | B | 6-10 |
| Module complexity | A | 1-5 |
| Average complexity | A | 1-5 |
### Deptry (Dependency Analysis)
```bash
uv run deptry .
```
Finds unused, missing, and misplaced dependencies.
### Semgrep (Static Analysis)
```bash
uv run semgrep --config=auto roboco/
```
Advanced pattern-based static analysis.
---
## Code Complexity
### PY-070: Maximum Cyclomatic Complexity
**Severity:** ERROR
**Tools:** radon, xenon
Functions MUST have cyclomatic complexity <= 10 (grade B or better).
```python
# Bad - Too complex (CC > 10)
def process_request(request: Request) -> Response:
if request.type == "A":
if request.priority == 1:
if request.urgent:
... # Deep nesting = high complexity
elif request.type == "B":
...
# Good - Decomposed into smaller functions
def process_request(request: Request) -> Response:
handler = get_handler(request.type)
return handler.process(request)
```
### PY-071: Maximum Function Length
**Severity:** WARNING
**Tools:** code review
Functions SHOULD be <= 50 lines. Consider decomposition if longer.
### PY-072: Maximum Arguments
**Severity:** WARNING
**Tools:** ruff (PLR0913)
Functions SHOULD have <= 5 arguments. Use dataclasses or Pydantic models for more.
```python
# Bad - Too many arguments
def create_task(
title: str,
description: str,
priority: int,
due_date: datetime,
assigned_to: str,
tags: list[str],
parent_id: str | None,
) -> Task:
...
# Good - Use a model
class TaskCreate(BaseModel):
title: str
description: str
priority: int = 1
due_date: datetime | None = None
assigned_to: str | None = None
tags: list[str] = []
parent_id: str | None = None
def create_task(params: TaskCreate) -> Task:
...
```
### PY-073: Avoid Deep Nesting
**Severity:** WARNING
**Tools:** code review
Maximum nesting depth SHOULD be 4 levels. Use early returns and guard clauses.
```python
# Bad - Deep nesting
def process(data: Data) -> Result:
if data.valid:
if data.type == "A":
if data.ready:
if data.value > 0:
return process_a(data)
return None
# Good - Guard clauses
def process(data: Data) -> Result | None:
if not data.valid:
return None
if data.type != "A":
return None
if not data.ready:
return None
if data.value <= 0:
return None
return process_a(data)
```
---
## Security
### PY-080: No Hardcoded Secrets
**Severity:** BLOCKER
**Tools:** bandit (B105, B106, B107)
NEVER hardcode secrets. Use environment variables.
```python
# Bad - NEVER do this
API_KEY = "sk-abc123xyz789"
DATABASE_URL = "postgresql://user:password@host/db"
# Good - Load from environment
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_key: str
database_url: str
model_config = {"env_prefix": "ROBOCO_"}
```
### PY-081: Use `usedforsecurity` for Non-Security Hashes
**Severity:** WARNING
**Tools:** bandit (B324)
When using hash functions for non-security purposes, add `usedforsecurity=False`.
```python
# Good - Non-security hash for ID generation
import hashlib
content_hash = hashlib.md5(
content.encode(),
usedforsecurity=False
).hexdigest()[:12]
```
### PY-082: SQL Injection Prevention
**Severity:** BLOCKER
**Tools:** bandit (B608), semgrep
NEVER construct SQL with string concatenation. Use parameterized queries.
```python
# Bad - SQL injection vulnerability
query = f"SELECT * FROM users WHERE id = '{user_id}'"
# Good - Parameterized query
result = await session.execute(
select(User).where(User.id == user_id)
)
```
### PY-083: Command Injection Prevention
**Severity:** BLOCKER
**Tools:** bandit (B602, B603, B604)
NEVER pass user input directly to shell commands.
```python
# Bad - Command injection vulnerability
import os
os.system(f"process_file {filename}")
# Good - Use subprocess with list
import subprocess
if not SAFE_FILENAME_PATTERN.match(filename):
raise ValidationError("Invalid filename")
subprocess.run(["process_file", filename], check=True)
```
### PY-084: No `eval` or `exec`
**Severity:** BLOCKER
**Tools:** bandit (B307)
NEVER use `eval()` or `exec()` on untrusted input.
```python
# Bad - Code injection vulnerability
result = eval(user_input)
# Good - Use ast.literal_eval for safe parsing
import ast
result = ast.literal_eval(user_input) # Only parses literals
```
---
## Quick Reference
### Before Committing
```bash
# Format
uv run ruff format .
# Lint
uv run ruff check .
# Type check
uv run mypy roboco/
# Dead code
uv run vulture roboco/ vulture_whitelist.py
# Full check (recommended)
make lint
```
### Severity Levels
| Level | Action | Blocks PR |
|-------|--------|-----------|
| BLOCKER | Must fix immediately | Yes |
| ERROR | Must fix before merge | Yes |
| WARNING | Should fix | No |
| INFO | Consider improving | No |
### Rule ID Reference
| Prefix | Category |
|--------|----------|
| PY-00X | Code style |
| PY-01X | Type safety |
| PY-02X | Error handling |
| PY-03X | Data validation |
| PY-04X | Async patterns |
| PY-05X | Testing |
| PY-06X | Dependencies |
| PY-07X | Complexity |
| PY-08X | Security |
+985
View File
@@ -0,0 +1,985 @@
# TypeScript Coding Standards
Comprehensive standards for TypeScript/React development in the RoboCo system.
---
## Table of Contents
1. [Code Style](#code-style)
2. [Type Safety](#type-safety)
3. [React Patterns](#react-patterns)
4. [State Management](#state-management)
5. [Error Handling](#error-handling)
6. [Testing](#testing)
7. [Build & Tools](#build--tools)
8. [Performance](#performance)
9. [Security](#security)
---
## Code Style
### TS-001: Strict Mode
**Severity:** ERROR
**Tools:** TypeScript compiler
All TypeScript files MUST use strict mode. No `any` types unless absolutely necessary.
```typescript
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true
}
}
```
### TS-002: Explicit Return Types
**Severity:** ERROR
**Tools:** ESLint (@typescript-eslint/explicit-function-return-type)
Functions MUST have explicit return type annotations.
```typescript
// Good
function processTask(taskId: string): Promise<TaskResult> {
// ...
}
async function fetchUser(userId: string): Promise<User | null> {
// ...
}
// Bad - Inferred return type
function processTask(taskId: string) {
// ...
}
```
### TS-003: Interface Over Type
**Severity:** WARNING
**Tools:** ESLint
Prefer `interface` for object shapes, `type` for unions/intersections.
```typescript
// Good - Object shape with interface
interface User {
id: string;
name: string;
email: string;
}
// Good - Interface for extending
interface AdminUser extends User {
permissions: Permission[];
}
// Good - Union type
type TaskStatus = 'pending' | 'in_progress' | 'completed';
// Good - Intersection type
type WithTimestamps<T> = T & {
createdAt: Date;
updatedAt: Date;
};
```
### TS-004: Naming Conventions
**Severity:** ERROR
**Tools:** ESLint
| Type | Convention | Example |
|------|------------|---------|
| Interfaces | PascalCase | `User`, `TaskService` |
| Types | PascalCase | `TaskStatus`, `ApiResponse` |
| Classes | PascalCase | `TaskManager`, `UserStore` |
| Functions | camelCase | `fetchUser`, `processTask` |
| Variables | camelCase | `userId`, `taskCount` |
| Constants | SCREAMING_SNAKE | `MAX_RETRIES`, `API_BASE_URL` |
| Enums | PascalCase | `TaskStatus`, `UserRole` |
| Components | PascalCase | `TaskCard`, `UserProfile` |
| Hooks | camelCase with use prefix | `useTask`, `useAuth` |
### TS-005: Import Organization
**Severity:** WARNING
**Tools:** ESLint (import/order)
Organize imports in this order: external, internal, relative.
```typescript
// External dependencies
import React, { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
// Internal aliases (configured in tsconfig)
import { TaskService } from '@/services/task';
import { Button } from '@/components/ui';
// Relative imports
import { TaskCard } from './TaskCard';
import type { TaskProps } from './types';
```
---
## Type Safety
### TS-010: No `any` Types
**Severity:** ERROR
**Tools:** ESLint (@typescript-eslint/no-explicit-any)
NEVER use `any`. Use `unknown`, generics, or proper types.
```typescript
// Bad
function processData(data: any): any {
return data.value;
}
// Good - Use unknown and narrow
function processData(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String(data.value);
}
throw new Error('Invalid data format');
}
// Good - Use generics
function processData<T extends { value: string }>(data: T): string {
return data.value;
}
```
### TS-011: Use Type Guards
**Severity:** WARNING
**Tools:** Code review
Create type guards for runtime type checking.
```typescript
// Type guard function
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
typeof (value as User).id === 'string'
);
}
// Usage
function processResponse(data: unknown): void {
if (isUser(data)) {
console.log(data.name); // TypeScript knows data is User
}
}
```
### TS-012: Discriminated Unions
**Severity:** WARNING
**Tools:** Code review
Use discriminated unions for state variants.
```typescript
// Good - Discriminated union for API states
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Usage with exhaustiveness checking
function renderState<T>(state: ApiState<T>): React.ReactNode {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <Spinner />;
case 'success':
return <DataView data={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
default:
// Exhaustiveness check
const _exhaustive: never = state;
return _exhaustive;
}
}
```
### TS-013: Const Assertions
**Severity:** WARNING
**Tools:** Code review
Use `as const` for immutable literal types.
```typescript
// Good - Const assertion for immutable config
const ROUTES = {
home: '/',
tasks: '/tasks',
settings: '/settings',
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// Type: '/' | '/tasks' | '/settings'
// Good - Const assertion for tuples
const tuple = [1, 'hello'] as const;
// Type: readonly [1, 'hello']
```
### TS-014: Avoid Type Assertions
**Severity:** WARNING
**Tools:** ESLint
Avoid type assertions (`as`). Use type guards or proper typing instead.
```typescript
// Bad - Type assertion
const user = response.data as User;
// Good - Validate with type guard
function isUser(data: unknown): data is User {
// ... validation
}
const user = isUser(response.data) ? response.data : null;
// Good - Zod schema validation
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const user = UserSchema.parse(response.data);
```
---
## React Patterns
### TS-020: Functional Components
**Severity:** ERROR
**Tools:** ESLint
Use functional components with hooks. No class components.
```typescript
// Good - Functional component with typed props
interface TaskCardProps {
task: Task;
onComplete: (taskId: string) => void;
className?: string;
}
const TaskCard: React.FC<TaskCardProps> = ({ task, onComplete, className }) => {
const [isLoading, setIsLoading] = useState(false);
const handleComplete = async (): Promise<void> => {
setIsLoading(true);
await completeTask(task.id);
onComplete(task.id);
setIsLoading(false);
};
return (
<div className={className}>
<h3>{task.title}</h3>
<Button onClick={handleComplete} disabled={isLoading}>
Complete
</Button>
</div>
);
};
```
### TS-021: Custom Hooks
**Severity:** WARNING
**Tools:** Code review
Extract reusable logic into custom hooks with `use` prefix.
```typescript
// Good - Custom hook with proper types
interface UseTaskResult {
task: Task | null;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useTask(taskId: string): UseTaskResult {
const [task, setTask] = useState<Task | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchTask = useCallback(async (): Promise<void> => {
setIsLoading(true);
setError(null);
try {
const data = await taskService.get(taskId);
setTask(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setIsLoading(false);
}
}, [taskId]);
useEffect(() => {
void fetchTask();
}, [fetchTask]);
return { task, isLoading, error, refetch: fetchTask };
}
```
### TS-022: Memoization
**Severity:** WARNING
**Tools:** ESLint (react-hooks/exhaustive-deps)
Use `useMemo` and `useCallback` appropriately.
```typescript
// Good - Memoize expensive computation
const sortedTasks = useMemo(() => {
return tasks.slice().sort((a, b) => b.priority - a.priority);
}, [tasks]);
// Good - Memoize callback for child components
const handleSelect = useCallback((taskId: string) => {
setSelectedId(taskId);
onTaskSelect?.(taskId);
}, [onTaskSelect]);
// Bad - Over-memoization (simple computation)
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// Just use: const fullName = `${first} ${last}`;
```
### TS-023: Event Handlers
**Severity:** WARNING
**Tools:** Code review
Type event handlers properly.
```typescript
// Good - Properly typed event handlers
const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
// ...
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setValue(event.target.value);
};
const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
event.stopPropagation();
// ...
};
```
### TS-024: Children Props
**Severity:** WARNING
**Tools:** Code review
Use `React.ReactNode` for children props.
```typescript
// Good - Typed children
interface LayoutProps {
children: React.ReactNode;
sidebar?: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children, sidebar }) => (
<div className="layout">
{sidebar && <aside>{sidebar}</aside>}
<main>{children}</main>
</div>
);
```
---
## State Management
### TS-030: Local State First
**Severity:** WARNING
**Tools:** Code review
Prefer local component state. Only lift state when necessary.
```typescript
// Good - Local state for component-specific data
function TaskEditor(): React.ReactElement {
const [draft, setDraft] = useState('');
const [isValid, setIsValid] = useState(false);
// This state doesn't need to be global
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
```
### TS-031: Server State with React Query
**Severity:** ERROR
**Tools:** Code review
Use React Query (TanStack Query) for server state management.
```typescript
// Good - React Query for API data
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TaskList(): React.ReactElement {
const queryClient = useQueryClient();
const { data: tasks, isLoading, error } = useQuery({
queryKey: ['tasks'],
queryFn: () => taskService.list(),
});
const createTask = useMutation({
mutationFn: taskService.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul>
{tasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</ul>
);
}
```
### TS-032: Zustand for Client State
**Severity:** WARNING
**Tools:** Code review
Use Zustand for global client state when needed.
```typescript
// Good - Zustand store with proper types
import { create } from 'zustand';
interface UIStore {
sidebarOpen: boolean;
theme: 'light' | 'dark';
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
theme: 'light',
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}));
```
---
## Error Handling
### TS-040: Error Boundaries
**Severity:** ERROR
**Tools:** Code review
Wrap major UI sections in error boundaries.
```typescript
// Error boundary component
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback: React.ReactNode;
}
class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error('Error boundary caught:', error, info);
}
render(): React.ReactNode {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorFallback />}>
<TaskDashboard />
</ErrorBoundary>
```
### TS-041: Type-Safe Error Handling
**Severity:** WARNING
**Tools:** Code review
Use discriminated unions for error states.
```typescript
// Result type for operations that can fail
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Usage
async function fetchTask(id: string): Promise<Result<Task>> {
try {
const task = await api.get(`/tasks/${id}`);
return { success: true, data: task };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err : new Error('Unknown error'),
};
}
}
// Consumer
const result = await fetchTask(id);
if (result.success) {
console.log(result.data.title);
} else {
console.error(result.error.message);
}
```
### TS-042: Catch Block Typing
**Severity:** ERROR
**Tools:** TypeScript (useUnknownInCatchVariables)
Handle `unknown` type in catch blocks.
```typescript
// Good - Handle unknown error type
try {
await riskyOperation();
} catch (error: unknown) {
if (error instanceof ApiError) {
handleApiError(error);
} else if (error instanceof Error) {
handleGenericError(error);
} else {
handleUnknownError(String(error));
}
}
```
---
## Testing
### TS-050: Testing Library
**Severity:** ERROR
**Tools:** Jest, Testing Library
Use React Testing Library. Test behavior, not implementation.
```typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('TaskCard', () => {
it('shows loading state while completing task', async () => {
const user = userEvent.setup();
const onComplete = vi.fn();
render(<TaskCard task={mockTask} onComplete={onComplete} />);
await user.click(screen.getByRole('button', { name: /complete/i }));
expect(screen.getByRole('button')).toBeDisabled();
await waitFor(() => {
expect(onComplete).toHaveBeenCalledWith(mockTask.id);
});
});
it('displays error message when task fails to load', async () => {
render(<TaskView taskId="invalid" />);
expect(await screen.findByRole('alert')).toHaveTextContent(/failed/i);
});
});
```
### TS-051: Mock External Dependencies
**Severity:** WARNING
**Tools:** Jest/Vitest
Mock API calls and external services.
```typescript
import { vi } from 'vitest';
import { taskService } from '@/services/task';
vi.mock('@/services/task');
describe('useTask', () => {
it('returns task data on success', async () => {
vi.mocked(taskService.get).mockResolvedValue(mockTask);
const { result } = renderHook(() => useTask('task-123'));
await waitFor(() => {
expect(result.current.task).toEqual(mockTask);
expect(result.current.isLoading).toBe(false);
});
});
});
```
### TS-052: Test Coverage
**Severity:** WARNING
**Tools:** c8/istanbul
Maintain minimum 80% test coverage.
```bash
# Run tests with coverage
pnpm test:coverage
```
---
## Build & Tools
### TS-060: Use PNPM
**Severity:** ERROR
**Tools:** pnpm
Use `pnpm` as the package manager.
```bash
# Install dependencies
pnpm install
# Add dependency
pnpm add package-name
# Add dev dependency
pnpm add -D package-name
```
### TS-061: ESLint Configuration
**Severity:** ERROR
**Tools:** ESLint
Use ESLint with TypeScript parser.
```javascript
// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
}
);
```
### TS-062: Prettier Configuration
**Severity:** WARNING
**Tools:** Prettier
Use Prettier for consistent formatting.
```json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true
}
```
### TS-063: TypeScript Configuration
**Severity:** ERROR
**Tools:** TypeScript
Use strict TypeScript configuration.
```json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}
```
---
## Performance
### TS-070: Lazy Loading
**Severity:** WARNING
**Tools:** React, Webpack/Vite
Use lazy loading for code splitting.
```typescript
import { lazy, Suspense } from 'react';
// Lazy load route components
const TaskDashboard = lazy(() => import('./pages/TaskDashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App(): React.ReactElement {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/tasks" element={<TaskDashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
```
### TS-071: Virtual Lists
**Severity:** WARNING
**Tools:** react-virtual, react-window
Use virtualization for long lists.
```typescript
import { useVirtualizer } from '@tanstack/react-virtual';
function TaskList({ tasks }: { tasks: Task[] }): React.ReactElement {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: tasks.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} className="overflow-auto h-screen">
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map((item) => (
<TaskItem key={tasks[item.index].id} task={tasks[item.index]} />
))}
</div>
</div>
);
}
```
---
## Security
### TS-080: XSS Prevention
**Severity:** BLOCKER
**Tools:** ESLint (react/no-danger)
NEVER use `dangerouslySetInnerHTML` with user input.
```typescript
// Bad - XSS vulnerability
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// Good - Use a sanitization library if HTML is required
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
// Good - Prefer text content
<div>{userContent}</div>
```
### TS-081: No `eval`
**Severity:** BLOCKER
**Tools:** ESLint (no-eval)
NEVER use `eval()` or `Function()` constructor.
```typescript
// Bad - Code injection vulnerability
eval(userInput);
new Function(userInput)();
// Good - Use proper parsing
JSON.parse(userInput);
```
### TS-082: Secure HTTP Calls
**Severity:** ERROR
**Tools:** Code review
Always use HTTPS for API calls.
```typescript
// Good - Secure configuration
const api = axios.create({
baseURL: process.env.API_URL, // Should be https://
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});
```
### TS-083: Environment Variables
**Severity:** ERROR
**Tools:** Code review
Never expose secrets in frontend code.
```typescript
// Good - Only public env vars exposed
const config = {
apiUrl: import.meta.env.VITE_API_URL,
publicKey: import.meta.env.VITE_PUBLIC_KEY,
};
// Bad - Never do this (even though it won't work)
const secret = import.meta.env.VITE_SECRET_KEY; // Exposed in browser!
```
---
## Quick Reference
### Before Committing
```bash
# Format
pnpm format
# Lint
pnpm lint
# Type check
pnpm typecheck
# Test
pnpm test
# All checks
pnpm check
```
### Severity Levels
| Level | Action | Blocks PR |
|-------|--------|-----------|
| BLOCKER | Must fix immediately | Yes |
| ERROR | Must fix before merge | Yes |
| WARNING | Should fix | No |
| INFO | Consider improving | No |
### Rule ID Reference
| Prefix | Category |
|--------|----------|
| TS-00X | Code style |
| TS-01X | Type safety |
| TS-02X | React patterns |
| TS-03X | State management |
| TS-04X | Error handling |
| TS-05X | Testing |
| TS-06X | Build & tools |
| TS-07X | Performance |
| TS-08X | Security |
+172
View File
@@ -0,0 +1,172 @@
# Security Standards (OWASP Top 10)
Security standards based on OWASP Top 10 for the RoboCo system.
## SEC-001: Injection Prevention
### SQL Injection
NEVER construct SQL queries with string concatenation.
```python
# NEVER DO THIS
query = f"SELECT * FROM users WHERE id = '{user_id}'"
# ALWAYS USE parameterized queries
result = await db.execute(
"SELECT * FROM users WHERE id = :id",
{"id": user_id}
)
```
### Command Injection
NEVER pass user input directly to shell commands.
```python
# NEVER DO THIS
os.system(f"process_file {filename}")
# ALWAYS validate and sanitize
if not SAFE_FILENAME_PATTERN.match(filename):
raise ValidationError("Invalid filename")
subprocess.run(["process_file", filename], check=True)
```
## SEC-002: Authentication
### Password Storage
NEVER store passwords in plain text. Use bcrypt or argon2.
### Token Security
- JWT tokens MUST have short expiration (15-60 minutes)
- Refresh tokens MUST be stored securely (httpOnly cookies)
- Always validate token signatures
### Session Management
- Generate new session IDs after login
- Implement session timeout
- Invalidate sessions on logout
## SEC-003: Sensitive Data Exposure
### Environment Variables
Store secrets in environment variables, NEVER in code.
```python
# NEVER DO THIS
API_KEY = "sk-abc123xyz789"
# ALWAYS load from environment
API_KEY = os.getenv("API_KEY")
if not API_KEY:
raise ConfigurationError("API_KEY not set")
```
### Logging
NEVER log sensitive data (passwords, tokens, PII).
```python
# NEVER log credentials
logger.info(f"User login: {username}, password: {password}")
# Log safely
logger.info("User login", username=username, masked_password="***")
```
## SEC-004: Access Control
### Authorization Checks
Verify permissions on EVERY request, not just at entry points.
```python
async def update_task(task_id: str, agent: Agent):
task = await get_task(task_id)
# ALWAYS check permissions
if not await can_modify_task(agent, task):
raise PermissionDenied("Cannot modify this task")
# Proceed with update
```
### Principle of Least Privilege
Agents should have minimum permissions needed for their role.
## SEC-005: Security Misconfiguration
### Headers
Set security headers on all responses:
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY`
- `Content-Security-Policy`
- `Strict-Transport-Security`
### CORS
Configure CORS strictly. Never use `*` in production.
```python
# Development only
origins = ["http://localhost:3000"]
# Production
origins = ["https://app.roboco.ai"]
```
## SEC-006: Cross-Site Scripting (XSS)
### Output Encoding
Always encode user input before rendering in HTML.
### Content Security Policy
Implement strict CSP to prevent inline scripts.
### React/JSX
Never use `dangerouslySetInnerHTML` with user input.
## SEC-007: Insecure Deserialization
### Pickle/Eval
NEVER use `pickle.loads()` or `eval()` on untrusted data.
### JSON Validation
Always validate JSON structure with Pydantic before processing.
## SEC-008: Vulnerable Dependencies
### Dependency Scanning
Run `pip-audit` and `npm audit` in CI pipeline.
### Updates
Keep dependencies updated. Review security advisories weekly.
## SEC-009: Logging & Monitoring
### Audit Trail
Log all security-relevant events:
- Authentication attempts
- Authorization failures
- Data access
- Configuration changes
### Alerting
Set up alerts for:
- Multiple failed login attempts
- Permission denied spikes
- Unusual access patterns
## SEC-010: API Security
### Rate Limiting
Implement rate limiting on all endpoints.
### Input Validation
Validate all input parameters:
- Type checking
- Length limits
- Format validation
- Range checks
```python
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
priority: int = Field(..., ge=1, le=5)
```
+444
View File
@@ -0,0 +1,444 @@
# Agent Roles and Permissions
Comprehensive reference for agent roles, permissions, and organizational structure in the RoboCo system. Derived from actual implementation in the codebase.
**Source Files:**
- Role Definitions: `roboco/models/base.py` (lines 56-78)
- Agent Config: `roboco/agents_config.py`
- Permissions Model: `roboco/models/permissions.py`
---
## Table of Contents
1. [Agent Roles](#agent-roles)
2. [Organizational Structure](#organizational-structure)
3. [Agent Roster](#agent-roster)
4. [Permission Levels](#permission-levels)
5. [Task Permissions](#task-permissions)
6. [Knowledge Base Permissions](#knowledge-base-permissions)
7. [Communication Permissions](#communication-permissions)
8. [Role Capabilities](#role-capabilities)
---
## Agent Roles
### ROLE-001: AgentRole Enum
**Source:** `roboco/models/base.py`
```python
class AgentRole(str, Enum):
# System (internal orchestrator operations)
SYSTEM = "system"
# Executive
CEO = "ceo"
# Board
PRODUCT_OWNER = "product_owner"
HEAD_MARKETING = "head_marketing"
AUDITOR = "auditor"
# Management
MAIN_PM = "main_pm"
CELL_PM = "cell_pm"
# Cell Members
DEVELOPER = "developer"
QA = "qa"
DOCUMENTER = "documenter"
```
### ROLE-002: Role Descriptions
| Role | Description | Count |
|------|-------------|-------|
| `ceo` | Human executive, final authority | 1 |
| `product_owner` | Product strategy and direction | 1 |
| `head_marketing` | Marketing and external comms | 1 |
| `auditor` | Silent observer, quality oversight | 1 |
| `main_pm` | Coordinates all cells | 1 |
| `cell_pm` | Manages a single cell | 3 |
| `developer` | Writes code | 5 |
| `qa` | Reviews and tests | 3 |
| `documenter` | Writes documentation | 3 |
| **Total** | | **19** |
---
## Organizational Structure
### ROLE-010: Hierarchy
```
CEO (Renzo - Human)
┌──────────────────────┼──────────────────────┐
│ │ │
Product Owner Head of Marketing Auditor
(Board) (Board) (Silent Observer)
│ │ │
└──────────────────────┼──────────────────────┘
Main PM
┌──────────────────────┼──────────────────────┐
│ │ │
Backend Cell Frontend Cell UX/UI Cell
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
PM DEV*2 QA PM DEV*2 QA PM DEV QA
DOC DOC DOC
```
### ROLE-011: Cells
| Cell | PM | Developers | QA | Documenter |
|------|-----|------------|-----|------------|
| Backend | be-pm | be-dev-1, be-dev-2 | be-qa | be-doc |
| Frontend | fe-pm | fe-dev-1, fe-dev-2 | fe-qa | fe-doc |
| UX/UI | ux-pm | ux-dev | ux-qa | ux-doc |
---
## Agent Roster
### ROLE-020: Complete Agent List
**Source:** `roboco/agents_config.py`
| Slug | Role | Cell | Team |
|------|------|------|------|
| `ceo` | ceo | - | executive |
| `product-owner` | product_owner | - | board |
| `head-marketing` | head_marketing | - | board |
| `auditor` | auditor | - | board |
| `main-pm` | main_pm | - | management |
| `be-pm` | cell_pm | backend | management |
| `fe-pm` | cell_pm | frontend | management |
| `ux-pm` | cell_pm | uxui | management |
| `be-dev-1` | developer | backend | developers |
| `be-dev-2` | developer | backend | developers |
| `fe-dev-1` | developer | frontend | developers |
| `fe-dev-2` | developer | frontend | developers |
| `ux-dev` | developer | uxui | developers |
| `be-qa` | qa | backend | qa |
| `fe-qa` | qa | frontend | qa |
| `ux-qa` | qa | uxui | qa |
| `be-doc` | documenter | backend | documentation |
| `fe-doc` | documenter | frontend | documentation |
| `ux-doc` | documenter | uxui | documentation |
---
## Permission Levels
### ROLE-030: Permission Hierarchy
**Source:** `roboco/models/permissions.py`
```python
ROLE_PERMISSION_LEVELS: dict[str, str] = {
"system": "CEO", # System/orchestrator - CEO-level access
"ceo": "CEO", # Full access
"product_owner": "BOARD", # Cross-org access
"head_marketing": "BOARD", # Cross-org access
"auditor": "AUDITOR", # Special: silent read all
"main_pm": "MAIN_PM", # All cells access
"cell_pm": "CELL_PM", # Own cell + PM channel
"developer": "CELL_MEMBER", # Own cell only
"qa": "CELL_MEMBER", # Own cell only
"documenter": "CELL_MEMBER", # Own cell only
}
```
### ROLE-031: Level Descriptions
| Level | Description | Scope |
|-------|-------------|-------|
| `CEO` | Full access to everything | Organization-wide |
| `BOARD` | Cross-organization access | Cross-cell |
| `AUDITOR` | Silent read access to all | Read-only, all channels |
| `MAIN_PM` | All cells access | All cells |
| `CELL_PM` | Own cell + PM channel | Single cell + PM |
| `CELL_MEMBER` | Own cell only | Single cell |
---
## Task Permissions
### ROLE-040: Task Permission Matrix
**Source:** `roboco/models/permissions.py`
| Role | VIEW_ALL | VIEW_OWN | CREATE | ASSIGN | CLAIM | UPDATE_OWN | CLOSE | CHANGE_PRIORITY |
|------|:--------:|:--------:|:------:|:------:|:-----:|:----------:|:-----:|:---------------:|
| system | X | | X | X | X | X | X | X |
| ceo | X | | X | X | | | X | X |
| product_owner | X | | X | X | | | X | X |
| head_marketing | X | | X | X | | | X | X |
| auditor | X | | X | X | | | X | X |
| main_pm | X | | X | X | X | X | X | X |
| cell_pm | | X | X | X | X | X | X | X |
| developer | | X | | | X | X | X | |
| qa | | X | | | X | X | | |
| documenter | | X | | | X | X | X | |
### ROLE-041: Key Task Capabilities
**Who can CREATE tasks:**
- ceo, product_owner, head_marketing, auditor
- main_pm, cell_pm
**Who can ASSIGN tasks:**
- ceo, product_owner, head_marketing, auditor
- main_pm, cell_pm
**Who can CLAIM tasks:**
- main_pm, cell_pm
- developer, qa, documenter (role-appropriate statuses)
**Who can CLOSE (complete) tasks:**
- ceo, product_owner, head_marketing, auditor
- main_pm, cell_pm
- developer, documenter (their own tasks)
**Who can CANCEL tasks:**
- cell_pm, main_pm, product_owner, head_marketing
- NOT ceo (by design)
- NOT auditor
---
## Knowledge Base Permissions
### ROLE-050: KB Permission Matrix
**Source:** `roboco/models/permissions.py`
| Role | INDEX_CODE | INDEX_DOCS | SEARCH | QUERY | VIEW_STATS | CLEAR | REFRESH |
|------|:----------:|:----------:|:------:|:-----:|:----------:|:-----:|:-------:|
| ceo | X | X | X | X | X | X | X |
| product_owner | | X | X | X | X | | |
| head_marketing | | X | X | X | | | |
| auditor | | | X | X | X | | |
| main_pm | X | X | X | X | X | X | X |
| cell_pm | X | X | X | X | X | | |
| developer | X | X | X | X | | | |
| qa | | | X | X | | | |
| documenter | | X | X | X | | | |
### ROLE-051: KB Capability Summary
**Who can INDEX_CODE:**
- ceo, main_pm, cell_pm, developer
**Who can INDEX_DOCS:**
- ceo, product_owner, head_marketing
- main_pm, cell_pm
- developer, documenter
**Who can SEARCH/QUERY:**
- Everyone
**Who can CLEAR_INDEX/REFRESH:**
- ceo, main_pm only
---
## Communication Permissions
### ROLE-060: Notification Permissions
**Who CAN send notifications:**
- cell_pm
- main_pm
- product_owner
- head_marketing
- auditor
- ceo
**Who CANNOT send notifications:**
- developer
- qa
- documenter
### ROLE-061: Channel Access
**Cell Channels** (e.g., `#backend-cell`):
- Read: Cell members + Main PM
- Write: Cell members
- Silent: Auditor
**Cross-Cell Channels** (e.g., `#dev-all`, `#qa-all`):
- Read/Write: Respective role members + Cell PMs + Main PM
- Silent: Auditor
**Management Channels** (e.g., `#main-pm-board`):
- Read/Write: Board + Main PM
- Silent: Auditor
**Broadcast Channels** (e.g., `#announcements`):
- Read: Everyone
- Write: PMs and Board only
### ROLE-062: Communication Matrix
Each role can communicate with:
| Role | Can Communicate With |
|------|---------------------|
| CEO | Everyone |
| Board Members | CEO, other board, Auditor, Main PM |
| Auditor | Everyone (silent read all channels) |
| Main PM | CEO, Board, Cell PMs |
| Cell PM | CEO, Auditor, Main PM, other Cell PMs, cell members |
| Cell Members | CEO, Auditor, own Cell PM, other cell members |
---
## Role Capabilities
### ROLE-070: Developer Capabilities
```markdown
CAN:
- Claim pending and needs_revision tasks
- Start, pause, resume work
- Submit for verification and QA
- Block tasks (with dependency)
- Search and query knowledge base
- Index code and documentation
- Journal their work
CANNOT:
- Create or assign tasks
- Pass/fail QA
- Complete tasks
- Cancel tasks
- Send notifications
- Clear/refresh KB indexes
```
### ROLE-071: QA Capabilities
```markdown
CAN:
- Claim awaiting_qa tasks
- Pass or fail QA
- Block tasks
- Search and query knowledge base
- Journal their work
CANNOT:
- Claim pending tasks
- Create or assign tasks
- Index content
- Complete documentation
- Complete tasks
- Cancel tasks
- Send notifications
```
### ROLE-072: Documenter Capabilities
```markdown
CAN:
- Claim awaiting_documentation tasks
- Complete documentation
- Index documentation
- Search and query knowledge base
- Journal their work
CANNOT:
- Claim pending tasks
- Create or assign tasks
- Index code
- Pass/fail QA
- Cancel tasks
- Send notifications
```
### ROLE-073: Cell PM Capabilities
```markdown
CAN:
- Create tasks in backlog
- Activate backlog → pending
- Assign tasks to cell members
- Complete awaiting_pm_review tasks
- Cancel any task (in cell)
- Unblock blocked tasks
- Send notifications
- Index code and documentation
- Full KB access (except clear/refresh)
CANNOT:
- Access other cells' tasks (unless Main PM)
- Clear/refresh KB indexes
```
### ROLE-074: Main PM Capabilities
```markdown
CAN:
- Everything Cell PM can do
- Access ALL cells
- Clear and refresh KB indexes
- Coordinate cross-cell work
```
### ROLE-075: Auditor Capabilities
```markdown
CAN:
- View all tasks
- View all channels (silent)
- Search and query knowledge base
- View KB stats
- Create tasks
- Assign tasks
CANNOT:
- Claim tasks
- Update tasks
- Clear KB indexes
- Write to most channels (silent observer)
```
---
## Quick Reference
### Role by Task Action
| Action | Allowed Roles |
|--------|---------------|
| Create task | CEO, Board, PMs |
| Activate task | PMs |
| Assign task | CEO, Board, PMs |
| Claim task | Developer, QA, Documenter, PMs |
| Pass QA | QA only |
| Fail QA | QA only |
| Complete docs | Documenter only |
| Complete task | PMs only |
| Cancel task | PMs, Board (not CEO, not Auditor) |
### Role Hierarchy
```
CEO
└─ Board (Product Owner, Head Marketing)
└─ Auditor (silent observer)
└─ Main PM
└─ Cell PMs
└─ Cell Members (Developer, QA, Documenter)
```
### Escalation Chain
```
Developer/QA/Documenter → Cell PM → Main PM → Product Owner → CEO
```
+467
View File
@@ -0,0 +1,467 @@
# Task Lifecycle Standards
Comprehensive standards for task management in the RoboCo system. These standards are derived from the actual implementation in the codebase.
**Source Files:**
- Task Status Enum: `roboco/models/base.py` (lines 19-34)
- Lifecycle Enforcement: `roboco/enforcement/task_lifecycle.py`
- Task Service: `roboco/services/task.py`
---
## Table of Contents
1. [Task States](#task-states)
2. [Valid Transitions](#valid-transitions)
3. [Role-Restricted Transitions](#role-restricted-transitions)
4. [Task Service Methods](#task-service-methods)
5. [Workflow by Role](#workflow-by-role)
6. [Quality Gates](#quality-gates)
7. [State Categories](#state-categories)
---
## Task States
### WF-001: TaskStatus Enum
**Source:** `roboco/models/base.py`
```python
class TaskStatus(str, Enum):
"""Task lifecycle states."""
BACKLOG = "backlog" # PM setup phase
PENDING = "pending" # Ready for work
CLAIMED = "claimed" # Agent has ownership
IN_PROGRESS = "in_progress" # Active work
BLOCKED = "blocked" # Waiting on dependency
PAUSED = "paused" # Temporarily stopped
VERIFYING = "verifying" # Self-verification
NEEDS_REVISION = "needs_revision"# QA rejected
AWAITING_QA = "awaiting_qa" # Ready for QA
AWAITING_DOCUMENTATION = "awaiting_documentation" # QA passed
AWAITING_PM_REVIEW = "awaiting_pm_review" # Docs done
COMPLETED = "completed" # TERMINAL
CANCELLED = "cancelled" # TERMINAL
```
### WF-002: State Diagram
```
PM CREATES
┌──────────┐
│ BACKLOG │───► cancelled
└────┬─────┘
│ activate()
┌──────────┐
┌──────│ PENDING │◄─────────────────────────────────┐
│ └────┬─────┘───► cancelled │
│ │ │
│ claim() │
│ │ │
▼ ▼ │
┌──────────┐ │
│ CLAIMED │───► pending, cancelled │
└────┬─────┘ │
│ start() │
▼ │
┌─────────────┐ │
┌──────────│ IN_PROGRESS │───► completed, cancelled │
│ └──────┬──────┘ │
│ │ │
block() pause() │
│ │ │
▼ ▼ │
┌──────────┐ ┌─────────┐ │
│ BLOCKED │ │ PAUSED │ │
└────┬─────┘ └────┬────┘ │
│ │ │
unblock() resume() │
│ │ │
└────────►───────►└──────►──────┐ │
│ │
submit_for_verification() │
│ │
▼ │
┌───────────┐ │
│ VERIFYING │───► cancelled │
└─────┬─────┘ │
│ │
┌───────────────────────┼───────────────────────┐ │
│ │ │ │
submit_for_qa() needs_revision direct to docs│
│ │ │ │
▼ ▼ ▼ │
┌─────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ AWAITING_QA │◄───────│ NEEDS_REVISION │──│ AWAITING_DOCUMENTATION│
└──────┬──────┘ └─────────────────┘ └──────────┬──────────┘
│ ▲ │
┌─────────┼─────────┐ │ │
│ │ │ │ docs_complete()
pass_qa() fail_qa() block │ │
│ │ │ │ ▼
│ └─────────┴───────────────┘ ┌────────────────────┐
│ │ AWAITING_PM_REVIEW │
└────────────────────────────────────────────────└─────────┬──────────┘
complete()
┌───────────┐
│ COMPLETED │
└───────────┘
```
---
## Valid Transitions
### WF-010: Transition Matrix
**Source:** `roboco/enforcement/task_lifecycle.py` (lines 17-56)
| From Status | Valid Next States |
|-------------|-------------------|
| `backlog` | pending, cancelled |
| `pending` | claimed, cancelled |
| `claimed` | in_progress, pending, cancelled |
| `in_progress` | blocked, paused, verifying, completed, cancelled |
| `blocked` | in_progress, cancelled |
| `paused` | in_progress, cancelled |
| `verifying` | awaiting_qa, needs_revision, awaiting_documentation, cancelled |
| `needs_revision` | claimed, in_progress, cancelled |
| `awaiting_qa` | claimed, awaiting_documentation, needs_revision, blocked, cancelled |
| `awaiting_documentation` | claimed, awaiting_pm_review, cancelled |
| `awaiting_pm_review` | claimed, completed, cancelled |
| `completed` | *(TERMINAL - no transitions)* |
| `cancelled` | *(TERMINAL - no transitions)* |
### WF-011: Invalid Transitions
Any transition NOT in the matrix above will raise `InvalidTransitionError`.
```python
from roboco.enforcement.task_lifecycle import validate_task_transition
# This will raise InvalidTransitionError
validate_task_transition(
TaskStatus.PENDING,
TaskStatus.COMPLETED, # Cannot skip the workflow!
agent_role="developer"
)
```
---
## Role-Restricted Transitions
### WF-020: Permission Matrix
**Source:** `roboco/enforcement/task_lifecycle.py` (lines 66-93)
Certain transitions require specific roles:
| Transition | Allowed Roles |
|------------|---------------|
| `backlog → pending` | cell_pm, main_pm, product_owner, head_marketing |
| `awaiting_qa → claimed` | qa |
| `awaiting_qa → awaiting_documentation` | qa |
| `awaiting_qa → needs_revision` | qa |
| `awaiting_documentation → claimed` | documenter |
| `awaiting_documentation → awaiting_pm_review` | documenter |
| `awaiting_pm_review → claimed` | cell_pm, main_pm, product_owner, head_marketing |
| `awaiting_pm_review → completed` | cell_pm, main_pm, product_owner, head_marketing |
| `in_progress → completed` | cell_pm, main_pm, product_owner, head_marketing |
| `* → cancelled` | cell_pm, main_pm, product_owner, head_marketing |
### WF-021: Role Validation
```python
from roboco.enforcement.task_lifecycle import can_agent_transition
# Check if role can make transition
if can_agent_transition(
current_status=TaskStatus.AWAITING_QA,
new_status=TaskStatus.AWAITING_DOCUMENTATION,
agent_role="developer" # False - only QA can do this
):
# proceed
```
---
## Task Service Methods
### WF-030: Status Change Methods
**Source:** `roboco/services/task.py`
| Method | Status Change | Calling Roles |
|--------|---------------|---------------|
| `activate()` | BACKLOG → PENDING | PM roles |
| `claim()` | → CLAIMED | developer, qa, documenter (based on current status) |
| `start()` | CLAIMED/PAUSED/NEEDS_REVISION → IN_PROGRESS | Owner |
| `block()` | IN_PROGRESS → BLOCKED | Owner |
| `soft_block()` | IN_PROGRESS → BLOCKED | Owner (external factor) |
| `unblock()` | BLOCKED → IN_PROGRESS | Owner or PM |
| `pause()` | IN_PROGRESS → PAUSED | Owner |
| `resume()` | PAUSED → IN_PROGRESS | Owner |
| `submit_for_verification()` | IN_PROGRESS → VERIFYING | Developer |
| `submit_for_qa()` | VERIFYING → AWAITING_QA | Developer |
| `pass_qa()` | AWAITING_QA → AWAITING_DOCUMENTATION | QA |
| `fail_qa()` | AWAITING_QA → NEEDS_REVISION | QA |
| `docs_complete()` | AWAITING_DOCUMENTATION → AWAITING_PM_REVIEW | Documenter |
| `submit_for_pm_review()` | IN_PROGRESS → AWAITING_PM_REVIEW | Any (non-dev tasks) |
| `complete()` | AWAITING_PM_REVIEW → COMPLETED | PM roles |
| `cancel()` | ANY → CANCELLED | PM roles |
### WF-031: Core Validation Method
All status changes go through `_validate_and_set_status()`:
```python
def _validate_and_set_status(
self,
task: TaskTable,
new_status: TaskStatus,
agent_role: str | None = None,
) -> None:
"""
Validate and set task status with lifecycle enforcement.
This is the single point of truth for status changes.
"""
```
---
## Workflow by Role
### WF-040: Developer Workflow
```
PENDING
│ claim()
CLAIMED
│ start()
IN_PROGRESS ←──────────────┐
│ │
│ submit_for_ │ (from NEEDS_REVISION)
│ verification() │
▼ │
VERIFYING │
│ │
│ submit_for_qa() │
▼ │
AWAITING_QA ──fail_qa()──► NEEDS_REVISION
│ │
│ pass_qa() (by QA) │ claim() + start()
▼ │
[QA/Docs workflow] └───────────────────┘
```
**Developer can:**
- Claim `pending` and `needs_revision` tasks
- Start, pause, resume work
- Submit for verification and QA
- Block (with dependency)
**Developer cannot:**
- Pass/fail QA (that's QA's job)
- Complete tasks (that's PM's job)
- Cancel tasks
### WF-041: QA Workflow
```
AWAITING_QA
│ claim()
CLAIMED (QA owns)
│ start()
IN_PROGRESS
├── pass_qa() ────► AWAITING_DOCUMENTATION
└── fail_qa() ────► NEEDS_REVISION (reassigned to developer)
```
**QA can:**
- Claim `awaiting_qa` tasks only
- Pass or fail QA
- Block tasks
**QA cannot:**
- Claim pending tasks (developers do that)
- Complete documentation
- Complete tasks
### WF-042: Documenter Workflow
```
AWAITING_DOCUMENTATION
│ claim()
CLAIMED (Documenter owns)
│ start()
IN_PROGRESS
│ docs_complete()
AWAITING_PM_REVIEW
```
**Documenter can:**
- Claim `awaiting_documentation` tasks
- Complete documentation
**Documenter cannot:**
- Claim pending tasks
- Pass/fail QA
- Complete tasks
### WF-043: PM Workflow
```
BACKLOG
│ activate()
PENDING
│ (developers claim)
...
AWAITING_PM_REVIEW
│ complete()
COMPLETED
```
**PM can:**
- Create tasks in backlog
- Activate backlog → pending
- Complete awaiting_pm_review tasks
- Cancel any task
- Unblock blocked tasks
---
## Quality Gates
### WF-050: Before Claiming
```markdown
- [ ] Task is in valid claim status for your role
- [ ] No existing in_progress task (one at a time)
- [ ] Dependencies are completed
```
### WF-051: Before Starting
```markdown
- [ ] Task is claimed by you
- [ ] Plan is documented
- [ ] Cell channel notified
```
### WF-052: Before Submit to QA
```markdown
- [ ] Tests passing: `uv run pytest`
- [ ] Linting clean: `uv run ruff check .`
- [ ] Type check: `uv run mypy roboco/`
- [ ] Self-review completed
- [ ] Journal reflection written
```
### WF-053: Before Completion
```markdown
- [ ] QA approved
- [ ] Documentation complete
- [ ] PM review approved
```
---
## State Categories
### WF-060: Helper Functions
**Source:** `roboco/enforcement/task_lifecycle.py`
| Function | Returns True For |
|----------|-----------------|
| `is_terminal_state()` | completed, cancelled |
| `is_waiting_state()` | blocked, paused, awaiting_qa, awaiting_documentation, awaiting_pm_review |
| `is_active_state()` | claimed, in_progress, verifying, needs_revision |
### WF-061: Terminal States
Once a task reaches `COMPLETED` or `CANCELLED`, no further transitions are possible.
```python
get_valid_transitions(TaskStatus.COMPLETED) # Returns []
get_valid_transitions(TaskStatus.CANCELLED) # Returns []
```
### WF-062: Waiting States
Tasks in waiting states are "on hold" pending some external action:
- `BLOCKED` - Waiting for blocker task
- `PAUSED` - Waiting for agent to resume
- `AWAITING_QA` - Waiting for QA review
- `AWAITING_DOCUMENTATION` - Waiting for docs
- `AWAITING_PM_REVIEW` - Waiting for PM approval
### WF-063: Active States
Tasks where an agent is actively working:
- `CLAIMED` - Agent owns, about to start
- `IN_PROGRESS` - Active development
- `VERIFYING` - Self-verification
- `NEEDS_REVISION` - Fixing QA issues
---
## Quick Reference
### Valid Claim Statuses by Role
| Role | Can Claim From |
|------|----------------|
| Developer | `pending`, `needs_revision` |
| QA | `awaiting_qa` |
| Documenter | `awaiting_documentation` |
| PM | `pending`, `backlog` |
### Common Status Flows
**Happy Path:**
```
backlog → pending → claimed → in_progress → verifying → awaiting_qa
→ awaiting_documentation → awaiting_pm_review → completed
```
**QA Rejection:**
```
awaiting_qa → needs_revision → claimed → in_progress → verifying → awaiting_qa
```
**Direct PM Review (non-dev task):**
```
pending → claimed → in_progress → awaiting_pm_review → completed
```
**Cancellation:**
```
any_state → cancelled
```