mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
RAG expansion + Optimal API
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
---
|
||||
|
||||
# CEO Directive: API-First Architecture Initiative
|
||||
|
||||
**From:** CEO
|
||||
**To:** Board (Product Owner, Head of Marketing, Auditor)
|
||||
**Priority:** High
|
||||
**Date:** 2024-12-23
|
||||
|
||||
---
|
||||
|
||||
## Strategic Context
|
||||
|
||||
We're positioning RoboCo for a two-phase growth strategy:
|
||||
|
||||
**Phase B (Now):** Two products, one backend. RoboCo Panel (full experience) and Codepanion (lightweight CLI, open source). Both consume the same API.
|
||||
|
||||
**Phase C (Future):** Platform play. The orchestration API becomes the product. Third parties build on us.
|
||||
|
||||
For this to work, **the API must be the only way in.** No backdoors. No direct DB access from frontends. No "just this once" shortcuts.
|
||||
|
||||
If we can't use our own API to build our own products, no one else can either.
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
**Establish API-first architecture across the entire system.**
|
||||
|
||||
This means:
|
||||
1. Every action the Panel takes goes through the public API
|
||||
2. Every action Codepanion will take goes through the public API
|
||||
3. Internal services communicate through well-defined interfaces
|
||||
4. The API is documented, consistent, and pleasant to use
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Zero direct database access from Panel frontend
|
||||
- [ ] Zero direct core imports that bypass API in frontend code
|
||||
- [ ] Codepanion can connect and perform basic operations (create session, send message, retrieve history)
|
||||
- [ ] API documentation covers all endpoints Codepanion needs
|
||||
- [ ] Auditor has verified no backdoors remain in critical paths
|
||||
|
||||
---
|
||||
|
||||
## Board Responsibilities
|
||||
|
||||
### Product Owner
|
||||
|
||||
1. **Define the API contract** for Codepanion integration
|
||||
- What endpoints does Codepanion need?
|
||||
- What's the minimal surface area?
|
||||
- What can wait for v2?
|
||||
|
||||
2. **Prioritize the cleanup work**
|
||||
- Which backdoors are blockers vs. nice-to-have?
|
||||
- What's the MVP for "API-first"?
|
||||
|
||||
3. **Approve the architecture decisions**
|
||||
- Review proposals from Main PM
|
||||
- Sign off on API design
|
||||
|
||||
### Head of Marketing
|
||||
|
||||
1. **Prepare positioning for Codepanion**
|
||||
- Open source CLI angle
|
||||
- "Works standalone, better connected" messaging
|
||||
- Developer-first tone
|
||||
|
||||
2. **Draft initial README/landing content**
|
||||
- What does Codepanion do?
|
||||
- Why would a dev use it?
|
||||
- How does it connect to RoboCo?
|
||||
|
||||
3. **Identify launch channels**
|
||||
- Where do we announce?
|
||||
- What's the content calendar?
|
||||
|
||||
### Auditor
|
||||
|
||||
1. **Conduct the backdoor audit**
|
||||
- Review all frontend → backend communication
|
||||
- Flag every direct DB access
|
||||
- Flag every import that bypasses API layer
|
||||
- Produce audit report with file, line, severity
|
||||
|
||||
2. **Verify fixes**
|
||||
- After cleanup, re-audit critical paths
|
||||
- Confirm API-first compliance
|
||||
|
||||
3. **Establish ongoing monitoring**
|
||||
- How do we prevent new backdoors?
|
||||
- What checks should be part of code review?
|
||||
|
||||
---
|
||||
|
||||
## Deliverables to Main PM
|
||||
|
||||
Once board alignment is complete, hand off to Main PM:
|
||||
|
||||
1. **Audit Report** (from Auditor)
|
||||
- List of all backdoors with severity ratings
|
||||
|
||||
2. **API Specification** (from Product Owner)
|
||||
- Endpoints needed for Codepanion MVP
|
||||
- Request/response schemas
|
||||
- Authentication approach
|
||||
|
||||
3. **Prioritized Task List** (from Product Owner)
|
||||
- Ordered by: blockers first, then high-value, then nice-to-have
|
||||
|
||||
4. **Marketing Brief** (from Head of Marketing)
|
||||
- Positioning document for Codepanion
|
||||
- README draft
|
||||
- Launch plan outline
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Timeline:** Codepanion MVP should be shippable within 2 weeks of Main PM receiving handoff
|
||||
- **Scope:** Fix what's necessary for Codepanion. Don't boil the ocean.
|
||||
- **Principle:** If in doubt, expose it through API. We'd rather have a slightly larger API surface than hidden backdoors.
|
||||
|
||||
---
|
||||
|
||||
## Notes on Documentation
|
||||
|
||||
For now:
|
||||
- Task documentation lives in the task's session (tied via session-task link)
|
||||
- Technical documentation (API docs, architecture) should be markdown in the repo
|
||||
- User-facing documentation (README, guides) prepared by Documenter roles, reviewed by PM
|
||||
|
||||
Future consideration: dedicated documentation system. But not now.
|
||||
|
||||
---
|
||||
|
||||
## Notes on Git Integration
|
||||
|
||||
Git integration is coming. For this initiative:
|
||||
- All code changes go through PRs
|
||||
- PRs link to task IDs in commit messages
|
||||
- Once git integration lands, this becomes automated
|
||||
|
||||
For now, manual discipline.
|
||||
|
||||
---
|
||||
|
||||
## Communication
|
||||
|
||||
- Board discussions in `management` channel
|
||||
- Main PM coordination in `cross-cell` channel
|
||||
- Cell work in respective cell channels (`backend-cell`, `frontend-cell`, `uxui-cell`)
|
||||
- Each task gets its own session within the appropriate channel
|
||||
- Journals capture decisions, learnings, blockers
|
||||
|
||||
---
|
||||
|
||||
## Final Word
|
||||
|
||||
This is the foundation. If we get API-first right, everything else becomes easier — Codepanion, platform expansion, third-party integrations, even our own Panel development.
|
||||
|
||||
If we get it wrong, we're building on sand.
|
||||
|
||||
Make it solid.
|
||||
|
||||
---
|
||||
|
||||
**CEO**
|
||||
@@ -1,436 +0,0 @@
|
||||
# RoboCo Workflows & Permissions
|
||||
|
||||
Visual documentation of task lifecycles, permissions, and workflows.
|
||||
|
||||
## 0. Terminology Dictionary
|
||||
|
||||
**CRITICAL: Understand these concepts before reading further.**
|
||||
|
||||
### Communication Hierarchy
|
||||
|
||||
```
|
||||
Channel → Group → Session → Messages
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ CHANNEL (e.g., "backend-cell") │
|
||||
│ A named communication space for a team/topic │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ GROUP (e.g., "Sprint 5 Backend Work") │ │
|
||||
│ │ A collection within a channel (project, sprint, topic) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ SESSION (e.g., "TASK-123 Discussion") │ │ │
|
||||
│ │ │ A conversation thread, usually tied to a task │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
|
||||
│ │ │ │ MESSAGES │ │ │ │
|
||||
│ │ │ │ Individual messages within a session │ │ │ │
|
||||
│ │ │ │ Types: action, dialogue, blocker, question │ │ │ │
|
||||
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
|
||||
│ │ └───────────────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Tools:
|
||||
- roboco_channel_list() → List channels you can access
|
||||
- roboco_channel_history() → Get messages from a channel's sessions
|
||||
- roboco_session_history_for_task() → Get messages for a specific task's session
|
||||
- roboco_message_send() → Send to channel (routes via task_id to session)
|
||||
```
|
||||
|
||||
### Journal vs Documentation
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┬─────────────────────────────────────┐
|
||||
│ JOURNAL │ DOCUMENTATION │
|
||||
├─────────────────────────────────────┼─────────────────────────────────────┤
|
||||
│ WHAT: Agent's personal reflections │ WHAT: Project/codebase docs │
|
||||
│ │ │
|
||||
│ WHERE: Database (journal entries) │ WHERE: /app/docs/ files │
|
||||
│ │ │
|
||||
│ WHO WRITES: Each agent, for self │ WHO WRITES: Documenter agents │
|
||||
│ │ │
|
||||
│ PURPOSE: │ PURPOSE: │
|
||||
│ - Track decisions & rationale │ - API documentation │
|
||||
│ - Record learnings & struggles │ - README updates │
|
||||
│ - Reflect on task completion │ - Changelog entries │
|
||||
│ - Build context for future sessions │ - Component/feature docs │
|
||||
│ │ │
|
||||
│ AUDIENCE: Self + cell members │ AUDIENCE: All developers, users │
|
||||
│ │ │
|
||||
│ TOOLS: │ TOOLS: │
|
||||
│ - roboco_journal_entry() │ - Write tool to /app/docs/ │
|
||||
│ - roboco_journal_reflect() │ - roboco_task_docs_complete() │
|
||||
│ - roboco_journal_decision() │ │
|
||||
│ - roboco_journal_learning() │ │
|
||||
│ - roboco_journal_read_team() │ │
|
||||
└─────────────────────────────────────┴─────────────────────────────────────┘
|
||||
|
||||
KEY DISTINCTION:
|
||||
- Journal = "What I learned/decided while doing this task" (internal notes)
|
||||
- Documentation = "How this feature works for others" (external docs)
|
||||
|
||||
Documenter reads journals to UNDERSTAND what was built, then writes documentation.
|
||||
```
|
||||
|
||||
### Task Notes vs Journals
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┬─────────────────────────────────────┐
|
||||
│ TASK NOTES │ JOURNALS │
|
||||
│ (dev_notes, qa_notes) │ │
|
||||
├─────────────────────────────────────┼─────────────────────────────────────┤
|
||||
│ Attached to the task record │ Separate entries linked by task_id │
|
||||
│ │ │
|
||||
│ Brief handoff summaries │ Detailed journey records │
|
||||
│ │ │
|
||||
│ Set via: │ Set via: │
|
||||
│ - roboco_task_submit_qa(notes) │ - roboco_journal_*() tools │
|
||||
│ - roboco_task_qa_pass(notes) │ │
|
||||
│ - roboco_task_docs_complete(notes) │ │
|
||||
│ │ │
|
||||
│ Examples: │ Examples: │
|
||||
│ "Implemented X, tested Y" │ "Decided to use pattern X because Y"│
|
||||
│ "QA passed, verified all criteria" │ "Struggled with Z, solved via W" │
|
||||
└─────────────────────────────────────┴─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 1. Task Lifecycle State Machine
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: Task Created
|
||||
|
||||
pending --> claimed: Developer claims
|
||||
pending --> cancelled: PM cancels
|
||||
|
||||
claimed --> in_progress: Developer starts
|
||||
claimed --> pending: Developer unclaims
|
||||
claimed --> cancelled: PM cancels
|
||||
|
||||
in_progress --> blocked: Developer blocked
|
||||
in_progress --> paused: Developer pauses
|
||||
in_progress --> verifying: Developer self-verifies
|
||||
in_progress --> cancelled: PM cancels
|
||||
|
||||
blocked --> in_progress: Unblocked
|
||||
blocked --> cancelled: PM cancels
|
||||
|
||||
paused --> in_progress: Developer resumes
|
||||
paused --> cancelled: PM cancels
|
||||
|
||||
verifying --> awaiting_qa: Submit for QA
|
||||
verifying --> needs_revision: Self-found issues
|
||||
verifying --> awaiting_documentation: Skip QA (small tasks)
|
||||
verifying --> cancelled: PM cancels
|
||||
|
||||
awaiting_qa --> awaiting_documentation: QA PASS
|
||||
awaiting_qa --> needs_revision: QA FAIL
|
||||
awaiting_qa --> blocked: Blocked during QA
|
||||
awaiting_qa --> cancelled: PM cancels
|
||||
|
||||
needs_revision --> in_progress: Developer resumes
|
||||
needs_revision --> cancelled: PM cancels
|
||||
|
||||
awaiting_documentation --> awaiting_pm_review: Documenter marks docs done
|
||||
awaiting_documentation --> cancelled: PM cancels
|
||||
|
||||
awaiting_pm_review --> completed: PM completes
|
||||
awaiting_pm_review --> cancelled: PM cancels
|
||||
|
||||
completed --> [*]
|
||||
cancelled --> [*]
|
||||
|
||||
quarantined --> pending: Un-quarantined
|
||||
```
|
||||
|
||||
## 2. Agent Hierarchy & Roles
|
||||
|
||||
```
|
||||
+-------+
|
||||
| CEO |
|
||||
+-------+
|
||||
|
|
||||
+---------------+---------------+
|
||||
| | |
|
||||
+---------+ +-----------+ +---------+
|
||||
| Product | | Head | | Auditor |
|
||||
| Owner | | Marketing | | (silent)|
|
||||
+---------+ +-----------+ +---------+
|
||||
| | |
|
||||
+-------+-------+ |
|
||||
| |
|
||||
+---------+ |
|
||||
| Main PM |<-----------------+
|
||||
+---------+ (observes all)
|
||||
|
|
||||
+-----------+-----------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
| BE PM | | FE PM | | UX PM |
|
||||
+-------+ +-------+ +-------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
|Backend| |Frontend| | UX/UI |
|
||||
| Cell | | Cell | | Cell |
|
||||
+-------+ +-------+ +-------+
|
||||
|
||||
Each Cell:
|
||||
- 2 Developers (BE/FE) or 1 Developer (UX)
|
||||
- 1 QA Engineer
|
||||
- 1 Documenter
|
||||
- 1 Cell PM
|
||||
```
|
||||
|
||||
## 3. Notification Permissions
|
||||
|
||||
```
|
||||
WHO CAN SEND NOTIFICATIONS:
|
||||
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Sender Role | Can Send? | Scope |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| CEO | YES | Anyone |
|
||||
| Auditor | YES | Anyone |
|
||||
| Main PM | YES | Anyone |
|
||||
| Product Owner | YES | main-pm, head-marketing, auditor, ceo |
|
||||
| Head Marketing | YES | main-pm, product-owner, auditor, ceo |
|
||||
| Cell PM | YES | Own cell only |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Developer | NO | - |
|
||||
| QA | NO | - |
|
||||
| Documenter | NO | - |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
|
||||
TOOLS VISIBILITY:
|
||||
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| Tool | Dev/QA/Doc | Cell PM | Main PM | Board | Aud/CEO |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| roboco_notify_list | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_get | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_ack | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_send | HIDDEN | YES | YES | YES | YES |
|
||||
| roboco_escalate | HIDDEN | YES | YES | HIDDEN | HIDDEN |
|
||||
| roboco_request_appr | HIDDEN | YES | YES | YES | HIDDEN |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
|
||||
Note: "Board" = Product Owner + Head Marketing. Auditor/CEO can send but not escalate or request approval.
|
||||
```
|
||||
|
||||
## 4. QA Workflow (Full Detail)
|
||||
|
||||
### QA Status Acceptance
|
||||
|
||||
QA can call `qa_pass` or `qa_fail` from ANY of these statuses:
|
||||
- `awaiting_qa` (initial)
|
||||
- `claimed` (after QA claims)
|
||||
- `in_progress` (after QA starts)
|
||||
|
||||
This allows QA to follow the full workflow: SCAN → CLAIM → PLAN → START → TEST → VERDICT
|
||||
|
||||
### QA Workflow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant QA as QA Engineer
|
||||
participant Doc as Documenter
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Submit for QA (awaiting_qa)
|
||||
Note over Task: assigned_to = QA<br/>quick_context = original_developer:Dev
|
||||
|
||||
QA->>Task: roboco_task_claim() → status = claimed
|
||||
QA->>Task: roboco_task_plan() → save test plan
|
||||
QA->>Task: roboco_task_start() → status = in_progress
|
||||
QA->>Task: Test work against criteria
|
||||
|
||||
alt QA PASS
|
||||
QA->>Task: roboco_task_qa_pass(qa_notes)
|
||||
Task->>Task: status = awaiting_documentation
|
||||
Note over Task: HANDOFF TO DOCUMENTER
|
||||
Doc->>Task: roboco_task_claim() → status = claimed
|
||||
Doc->>Task: roboco_task_plan() → save doc plan
|
||||
Doc->>Task: roboco_task_start() → status = in_progress
|
||||
Doc->>Task: GATHER info (see section 4a)
|
||||
Doc->>Task: WRITE docs to /app/docs/
|
||||
Doc->>Task: roboco_task_docs_complete()
|
||||
Task->>Task: status = awaiting_pm_review
|
||||
PM->>Task: roboco_task_complete()
|
||||
Task->>Task: status = completed
|
||||
else QA FAIL
|
||||
QA->>Task: roboco_task_qa_fail(qa_notes, issues)
|
||||
Task->>Task: status = needs_revision
|
||||
Task->>Task: assigned_to = original Dev
|
||||
Dev->>Task: roboco_task_start() → status = in_progress
|
||||
Dev->>Task: Fix issues, resubmit
|
||||
end
|
||||
```
|
||||
|
||||
### 4a. Documenter Information Gathering
|
||||
|
||||
```
|
||||
DOCUMENTER MUST GATHER FROM 3 SOURCES:
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SOURCE 1: TASK DETAILS (roboco_task_get) │
|
||||
│ ───────────────────────────────────────── │
|
||||
│ • description, acceptance_criteria │
|
||||
│ • dev_notes (developer's handoff summary) │
|
||||
│ • qa_notes (QA's verification notes) │
|
||||
│ • quick_context (original developer, etc.) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ SOURCE 2: TEAM JOURNALS (roboco_journal_read_team) │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ Read developer's journey: │
|
||||
│ roboco_journal_read_team("be-dev-1", task_id=task_id, limit=20) │
|
||||
│ │
|
||||
│ What to look for: │
|
||||
│ • Decisions made and WHY │
|
||||
│ • Struggles encountered and how solved │
|
||||
│ • Learnings documented │
|
||||
│ • Design/architecture choices │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ SOURCE 3: SESSION MESSAGES (roboco_session_history_for_task) │
|
||||
│ ──────────────────────────────────────────────────────────── │
|
||||
│ Read discussion context: │
|
||||
│ roboco_session_history_for_task(task_id) │
|
||||
│ │
|
||||
│ What to look for: │
|
||||
│ • Questions asked and answers given │
|
||||
│ • Clarifications from PM/dev │
|
||||
│ • Blockers discussed and resolutions │
|
||||
│ • Design decisions made in discussion │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
NOTE: Journals are INPUT for understanding. Documentation is OUTPUT to /app/docs/.
|
||||
```
|
||||
|
||||
## 5. Block/Unblock Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Working on task (in_progress)
|
||||
|
||||
Note over Dev: Encounters blocker
|
||||
|
||||
Dev->>Task: roboco_task_block(reason, type, what_needed)
|
||||
Task->>Task: POST /tasks/{id}/soft-block
|
||||
Task->>Task: status = blocked
|
||||
Task->>Task: dev_notes += blocker info
|
||||
|
||||
Note over Dev: Can work on other tasks
|
||||
|
||||
alt Blocker resolved
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
Task->>Task: POST /tasks/{id}/unblock
|
||||
Task->>Task: status = in_progress
|
||||
Dev->>Task: Continue working
|
||||
else Need PM help
|
||||
Dev->>PM: roboco_report_blocker() via message channel
|
||||
PM->>Task: Resolves blocker
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
end
|
||||
```
|
||||
|
||||
## 6. Task Role Restrictions
|
||||
|
||||
```
|
||||
ROLE-BASED TRANSITIONS:
|
||||
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| Transition | Allowed Roles |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| awaiting_qa → awaiting_doc | QA only |
|
||||
| awaiting_qa → needs_rev | QA only |
|
||||
| awaiting_doc → awaiting_pm | Documenter only |
|
||||
| awaiting_pm → completed | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
| * → cancelled | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
|
||||
Note: CEO and Auditor are NOT in the cancel/complete roles list - they observe but don't directly act on tasks.
|
||||
|
||||
VALID START STATUSES (for roboco_task_start):
|
||||
|
||||
+------------------+------------------------------------------+
|
||||
| Status | Who Can Start |
|
||||
+------------------+------------------------------------------+
|
||||
| claimed | Assigned agent (requires plan) |
|
||||
| paused | Assigned agent (resume) |
|
||||
| needs_revision | Original developer (fix QA issues) |
|
||||
+------------------+------------------------------------------+
|
||||
|
||||
QA VERDICT ACCEPTANCE (qa_pass/qa_fail):
|
||||
|
||||
+------------------+------------------------------------------+
|
||||
| Status | Reason |
|
||||
+------------------+------------------------------------------+
|
||||
| awaiting_qa | Task submitted for QA review |
|
||||
| claimed | QA claimed the task |
|
||||
| in_progress | QA started working (plan + start) |
|
||||
+------------------+------------------------------------------+
|
||||
|
||||
All three statuses are valid because QA follows the full workflow:
|
||||
awaiting_qa → claim → plan → start → test → verdict
|
||||
|
||||
DOCUMENTER COMPLETION ACCEPTANCE (docs_complete):
|
||||
|
||||
+----------------------+------------------------------------------+
|
||||
| Status | Reason |
|
||||
+----------------------+------------------------------------------+
|
||||
| awaiting_documentation | Task ready for documentation |
|
||||
| claimed | Documenter claimed the task |
|
||||
| in_progress | Documenter started (plan + start) |
|
||||
+----------------------+------------------------------------------+
|
||||
|
||||
All three statuses are valid because Documenter follows the full workflow:
|
||||
awaiting_documentation → claim → plan → start → gather → write → docs_complete
|
||||
```
|
||||
|
||||
## 7. Escalation Chain
|
||||
|
||||
```
|
||||
Developer/QA/Doc → Cell PM → Main PM → Product Owner → CEO
|
||||
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
| be-dev-1 |---->| | | | | | | |
|
||||
| be-dev-2 |---->| be-pm |---->| | | | | |
|
||||
| be-qa |---->| | | | | | | |
|
||||
| be-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| main-pm |---->| product-owner |---->| CEO |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| fe-dev-1 |---->| | | | | | | |
|
||||
| fe-dev-2 |---->| fe-pm |---->| | | | | |
|
||||
| fe-qa |---->| | | | | | | |
|
||||
| fe-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
```
|
||||
|
||||
## 8. Communication vs Notification
|
||||
|
||||
```
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Mechanism | Who Can Use | Purpose |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Messages | Everyone | Constant stream, logged |
|
||||
| (roboco_message) | | discussions, updates |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Blocker Reports | Everyone | Signal blocked status |
|
||||
| (roboco_report_ | | PM auto-notified |
|
||||
| blocker) | | |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Notifications | PM, Board, Auditor, CEO | Formal signals requiring |
|
||||
| (roboco_notify) | | acknowledgment |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Escalations | PMs only | High-priority issues |
|
||||
| (roboco_escalate) | | up the chain |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# Backend Documentation
|
||||
|
||||
Documentation for the Backend Cell team.
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: Backend Cell (developers, QA, PM, documenter)
|
||||
- **WRITE**: be-doc only
|
||||
|
||||
## Contents
|
||||
|
||||
- `/api/` - API documentation
|
||||
- `/qa/` - QA-related docs
|
||||
|
||||
## Contributing
|
||||
|
||||
Backend team members should request documentation updates through the Cell PM.
|
||||
Only the Backend Documenter (be-doc) can write to this directory.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Bug Documentation
|
||||
|
||||
Documentation for bugs organized by team.
|
||||
|
||||
## Structure
|
||||
|
||||
- `/backend/` - Backend bugs (be-doc writes)
|
||||
- `/frontend/` - Frontend bugs (fe-doc writes)
|
||||
- `/ux_ui/` - UX/UI bugs (ux-doc writes)
|
||||
- `/resolved/` - Archive for resolved bugs
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: All agents
|
||||
- **WRITE**: Team-specific documenters
|
||||
|
||||
## Bug Documentation Template
|
||||
|
||||
When documenting a bug:
|
||||
1. Description and reproduction steps
|
||||
2. Root cause analysis
|
||||
3. Solution implemented
|
||||
4. Prevention measures
|
||||
@@ -0,0 +1,15 @@
|
||||
# Feature Documentation
|
||||
|
||||
Documentation for features organized by team.
|
||||
|
||||
## Structure
|
||||
|
||||
- `/backend/` - Backend features (be-doc writes)
|
||||
- `/frontend/` - Frontend features (fe-doc writes)
|
||||
- `/ux_ui/` - UX/UI features (ux-doc writes)
|
||||
- `/shared/` - Cross-team features (any documenter writes)
|
||||
|
||||
## Access
|
||||
|
||||
Each team subdirectory follows the same access pattern as the main team docs.
|
||||
The `/shared/` directory is readable by all and writable by any documenter.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Frontend Documentation
|
||||
|
||||
Documentation for the Frontend Cell team.
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: Frontend Cell (developers, QA, PM, documenter)
|
||||
- **WRITE**: fe-doc only
|
||||
|
||||
## Contents
|
||||
|
||||
- `/components/` - Component documentation
|
||||
- `/qa/` - QA-related docs
|
||||
|
||||
## Contributing
|
||||
|
||||
Frontend team members should request documentation updates through the Cell PM.
|
||||
Only the Frontend Documenter (fe-doc) can write to this directory.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
# Initiatives Documentation
|
||||
|
||||
Cross-team initiatives and major projects.
|
||||
|
||||
## Structure
|
||||
|
||||
Each initiative gets its own subdirectory:
|
||||
```
|
||||
initiatives/
|
||||
├── {initiative-id}/
|
||||
│ ├── README.md
|
||||
│ ├── design.md
|
||||
│ ├── progress.md
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: Assigned teams
|
||||
- **WRITE**: Assigned documenters
|
||||
|
||||
## Creating an Initiative
|
||||
|
||||
1. Main PM creates initiative task
|
||||
2. Creates initiative directory
|
||||
3. Assigns teams and documenters
|
||||
4. Teams document their contributions
|
||||
@@ -0,0 +1,22 @@
|
||||
# RoboCo Self Documentation
|
||||
|
||||
Meta-documentation about the RoboCo system itself.
|
||||
|
||||
## Structure
|
||||
|
||||
- `/architecture/` - System architecture docs
|
||||
- `/incidents/` - Incident reports and post-mortems
|
||||
- `/reports/` - System reports and analytics
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: Board + PM roles
|
||||
- **WRITE**: Main PM, Auditor
|
||||
|
||||
## Purpose
|
||||
|
||||
This directory contains documentation about RoboCo's own operations:
|
||||
- How the system works
|
||||
- Issues encountered
|
||||
- Performance reports
|
||||
- Architectural decisions
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# UX/UI Documentation
|
||||
|
||||
Documentation for the UX/UI Cell team.
|
||||
|
||||
## Access
|
||||
|
||||
- **READ**: UX/UI Cell (developer, QA, PM, documenter)
|
||||
- **WRITE**: ux-doc only
|
||||
|
||||
## Contents
|
||||
|
||||
- `/design/` - Design system documentation
|
||||
- `/qa/` - QA-related docs
|
||||
|
||||
## Contributing
|
||||
|
||||
UX/UI team members should request documentation updates through the Cell PM.
|
||||
Only the UX/UI Documenter (ux-doc) can write to this directory.
|
||||
Reference in New Issue
Block a user