docs: enterprise refactor plan with ralph specs
This commit is contained in:
1
.ralph/.loop_start_sha
Normal file
1
.ralph/.loop_start_sha
Normal file
@@ -0,0 +1 @@
|
||||
4c92712d204993bdd6ff2a7b60583e28ac4f87b1
|
||||
185
.ralph/AGENT.md
185
.ralph/AGENT.md
@@ -1,158 +1,69 @@
|
||||
# Agent Build Instructions
|
||||
# ABE — Build, Test & Development Commands
|
||||
|
||||
## Project Setup
|
||||
## Install dependencies
|
||||
```bash
|
||||
# Install dependencies (example for Node.js project)
|
||||
npm install
|
||||
|
||||
# Or for Python project
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or for Rust project
|
||||
cargo build
|
||||
cd frontend && npm install && cd ..
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
## Build (backend)
|
||||
```bash
|
||||
# Node.js
|
||||
npm test
|
||||
|
||||
# Python
|
||||
pytest
|
||||
|
||||
# Rust
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
```bash
|
||||
# Production build
|
||||
npm run build
|
||||
# or
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Development Server
|
||||
## Build (frontend)
|
||||
```bash
|
||||
# Start development server
|
||||
npm run dev
|
||||
# or
|
||||
cargo run
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
## Key Learnings
|
||||
- Update this section when you learn new build optimizations
|
||||
- Document any gotchas or special setup requirements
|
||||
- Keep track of the fastest test/build cycle
|
||||
## Test
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Feature Development Quality Standards
|
||||
## Lint
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
**CRITICAL**: All new features MUST meet the following mandatory requirements before being considered complete.
|
||||
## Type check
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Testing Requirements
|
||||
## Dev mode
|
||||
```bash
|
||||
npm run dev # backend con hot reload
|
||||
cd frontend && npm run dev # frontend dev server
|
||||
```
|
||||
|
||||
- **Minimum Coverage**: 85% code coverage ratio required for all new code
|
||||
- **Test Pass Rate**: 100% - all tests must pass, no exceptions
|
||||
- **Test Types Required**:
|
||||
- Unit tests for all business logic and services
|
||||
- Integration tests for API endpoints or main functionality
|
||||
- End-to-end tests for critical user workflows
|
||||
- **Coverage Validation**: Run coverage reports before marking features complete:
|
||||
```bash
|
||||
# Examples by language/framework
|
||||
npm run test:coverage
|
||||
pytest --cov=src tests/ --cov-report=term-missing
|
||||
cargo tarpaulin --out Html
|
||||
```
|
||||
- **Test Quality**: Tests must validate behavior, not just achieve coverage metrics
|
||||
- **Test Documentation**: Complex test scenarios must include comments explaining the test strategy
|
||||
## Database
|
||||
```bash
|
||||
npm run db:migrate # ejecutar migraciones Kysely
|
||||
```
|
||||
|
||||
### Git Workflow Requirements
|
||||
## Docker
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
docker compose logs -f
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Before moving to the next feature, ALL changes must be:
|
||||
## Verificación completa (ejecutar después de CADA tarea)
|
||||
```bash
|
||||
npm run build && cd frontend && npm run build && cd .. && npm run test
|
||||
```
|
||||
|
||||
1. **Committed with Clear Messages**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat(module): descriptive message following conventional commits"
|
||||
```
|
||||
- Use conventional commit format: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, etc.
|
||||
- Include scope when applicable: `feat(api):`, `fix(ui):`, `test(auth):`
|
||||
- Write descriptive messages that explain WHAT changed and WHY
|
||||
## Commit después de tarea completada
|
||||
```bash
|
||||
git add -A && git commit -m "fase(X.Y): descripción"
|
||||
```
|
||||
|
||||
2. **Pushed to Remote Repository**:
|
||||
```bash
|
||||
git push origin <branch-name>
|
||||
```
|
||||
- Never leave completed features uncommitted
|
||||
- Push regularly to maintain backup and enable collaboration
|
||||
- Ensure CI/CD pipelines pass before considering feature complete
|
||||
## Notas
|
||||
- Source code: src/
|
||||
- Frontend: frontend/
|
||||
- Tests: junto al código (*.test.ts) o en tests/
|
||||
- Reports output: reports/
|
||||
- Logs: logs/
|
||||
- Database: data/abe.db
|
||||
|
||||
3. **Branch Hygiene**:
|
||||
- Work on feature branches, never directly on `main`
|
||||
- Branch naming convention: `feature/<feature-name>`, `fix/<issue-name>`, `docs/<doc-update>`
|
||||
- Create pull requests for all significant changes
|
||||
|
||||
4. **Ralph Integration**:
|
||||
- Update .ralph/fix_plan.md with new tasks before starting work
|
||||
- Mark items complete in .ralph/fix_plan.md upon completion
|
||||
- Update .ralph/PROMPT.md if development patterns change
|
||||
- Test features work within Ralph's autonomous loop
|
||||
|
||||
### Documentation Requirements
|
||||
|
||||
**ALL implementation documentation MUST remain synchronized with the codebase**:
|
||||
|
||||
1. **Code Documentation**:
|
||||
- Language-appropriate documentation (JSDoc, docstrings, etc.)
|
||||
- Update inline comments when implementation changes
|
||||
- Remove outdated comments immediately
|
||||
|
||||
2. **Implementation Documentation**:
|
||||
- Update relevant sections in this AGENT.md file
|
||||
- Keep build and test commands current
|
||||
- Update configuration examples when defaults change
|
||||
- Document breaking changes prominently
|
||||
|
||||
3. **README Updates**:
|
||||
- Keep feature lists current
|
||||
- Update setup instructions when dependencies change
|
||||
- Maintain accurate command examples
|
||||
- Update version compatibility information
|
||||
|
||||
4. **AGENT.md Maintenance**:
|
||||
- Add new build patterns to relevant sections
|
||||
- Update "Key Learnings" with new insights
|
||||
- Keep command examples accurate and tested
|
||||
- Document new testing patterns or quality gates
|
||||
|
||||
### Feature Completion Checklist
|
||||
|
||||
Before marking ANY feature as complete, verify:
|
||||
|
||||
- [ ] All tests pass with appropriate framework command
|
||||
- [ ] Code coverage meets 85% minimum threshold
|
||||
- [ ] Coverage report reviewed for meaningful test quality
|
||||
- [ ] Code formatted according to project standards
|
||||
- [ ] Type checking passes (if applicable)
|
||||
- [ ] All changes committed with conventional commit messages
|
||||
- [ ] All commits pushed to remote repository
|
||||
- [ ] .ralph/fix_plan.md task marked as complete
|
||||
- [ ] Implementation documentation updated
|
||||
- [ ] Inline code comments updated or added
|
||||
- [ ] .ralph/AGENT.md updated (if new patterns introduced)
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Features tested within Ralph loop (if applicable)
|
||||
- [ ] CI/CD pipeline passes
|
||||
|
||||
### Rationale
|
||||
|
||||
These standards ensure:
|
||||
- **Quality**: High test coverage and pass rates prevent regressions
|
||||
- **Traceability**: Git commits and .ralph/fix_plan.md provide clear history of changes
|
||||
- **Maintainability**: Current documentation reduces onboarding time and prevents knowledge loss
|
||||
- **Collaboration**: Pushed changes enable team visibility and code review
|
||||
- **Reliability**: Consistent quality gates maintain production stability
|
||||
- **Automation**: Ralph integration ensures continuous development practices
|
||||
|
||||
**Enforcement**: AI agents should automatically apply these standards to all feature development tasks without requiring explicit instruction for each task.
|
||||
|
||||
410
.ralph/PROMPT.md
410
.ralph/PROMPT.md
@@ -1,296 +1,182 @@
|
||||
# Ralph Development Instructions
|
||||
|
||||
## Context
|
||||
You are Ralph, an autonomous AI development agent working on a [YOUR PROJECT NAME] project.
|
||||
|
||||
## Current Objectives
|
||||
1. Study .ralph/specs/* to learn about the project specifications
|
||||
2. Review .ralph/fix_plan.md for current priorities
|
||||
3. Implement the highest priority item using best practices
|
||||
4. Use parallel subagents for complex tasks (max 100 concurrent)
|
||||
5. Run tests after each implementation
|
||||
6. Update documentation and fix_plan.md
|
||||
|
||||
## Key Principles
|
||||
- ONE task per loop - focus on the most important thing
|
||||
- Search the codebase before assuming something isn't implemented
|
||||
- Use subagents for expensive operations (file searching, analysis)
|
||||
- Write comprehensive tests with clear documentation
|
||||
- Update .ralph/fix_plan.md with your learnings
|
||||
- Commit working changes with descriptive messages
|
||||
|
||||
## Protected Files (DO NOT MODIFY)
|
||||
The following files and directories are part of Ralph's infrastructure.
|
||||
NEVER delete, move, rename, or overwrite these under any circumstances:
|
||||
- .ralph/ (entire directory and all contents)
|
||||
- .ralphrc (project configuration)
|
||||
|
||||
When performing cleanup, refactoring, or restructuring tasks:
|
||||
- These files are NOT part of your project code
|
||||
- They are Ralph's internal control files that keep the development loop running
|
||||
- Deleting them will break Ralph and halt all autonomous development
|
||||
|
||||
## 🧪 Testing Guidelines (CRITICAL)
|
||||
- LIMIT testing to ~20% of your total effort per loop
|
||||
- PRIORITIZE: Implementation > Documentation > Tests
|
||||
- Only write tests for NEW functionality you implement
|
||||
- Do NOT refactor existing tests unless broken
|
||||
- Do NOT add "additional test coverage" as busy work
|
||||
- Focus on CORE functionality first, comprehensive testing later
|
||||
|
||||
## Execution Guidelines
|
||||
- Before making changes: search codebase using subagents
|
||||
- After implementation: run ESSENTIAL tests for the modified code only
|
||||
- If tests fail: fix them as part of your current work
|
||||
- Keep .ralph/AGENT.md updated with build/run instructions
|
||||
- Document the WHY behind tests and implementations
|
||||
- No placeholder implementations - build it properly
|
||||
|
||||
## 🎯 Status Reporting (CRITICAL - Ralph needs this!)
|
||||
|
||||
**IMPORTANT**: At the end of your response, ALWAYS include this status block:
|
||||
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS | COMPLETE | BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: <number>
|
||||
FILES_MODIFIED: <number>
|
||||
TESTS_STATUS: PASSING | FAILING | NOT_RUN
|
||||
WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
|
||||
EXIT_SIGNAL: false | true
|
||||
RECOMMENDATION: <one line summary of what to do next>
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
### When to set EXIT_SIGNAL: true
|
||||
|
||||
Set EXIT_SIGNAL to **true** when ALL of these conditions are met:
|
||||
1. ✅ All items in fix_plan.md are marked [x]
|
||||
2. ✅ All tests are passing (or no tests exist for valid reasons)
|
||||
3. ✅ No errors or warnings in the last execution
|
||||
4. ✅ All requirements from specs/ are implemented
|
||||
5. ✅ You have nothing meaningful left to implement
|
||||
|
||||
### Examples of proper status reporting:
|
||||
|
||||
**Example 1: Work in progress**
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS
|
||||
TASKS_COMPLETED_THIS_LOOP: 2
|
||||
FILES_MODIFIED: 5
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: IMPLEMENTATION
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: Continue with next priority task from fix_plan.md
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Example 2: Project complete**
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: COMPLETE
|
||||
TASKS_COMPLETED_THIS_LOOP: 1
|
||||
FILES_MODIFIED: 1
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: DOCUMENTATION
|
||||
EXIT_SIGNAL: true
|
||||
RECOMMENDATION: All requirements met, project ready for review
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Example 3: Stuck/blocked**
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: 0
|
||||
FILES_MODIFIED: 0
|
||||
TESTS_STATUS: FAILING
|
||||
WORK_TYPE: DEBUGGING
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: Need human help - same error for 3 loops
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
### What NOT to do:
|
||||
- ❌ Do NOT continue with busy work when EXIT_SIGNAL should be true
|
||||
- ❌ Do NOT run tests repeatedly without implementing new features
|
||||
- ❌ Do NOT refactor code that is already working fine
|
||||
- ❌ Do NOT add features not in the specifications
|
||||
- ❌ Do NOT forget to include the status block (Ralph depends on it!)
|
||||
|
||||
## 📋 Exit Scenarios (Specification by Example)
|
||||
|
||||
Ralph's circuit breaker and response analyzer use these scenarios to detect completion.
|
||||
Each scenario shows the exact conditions and expected behavior.
|
||||
|
||||
### Scenario 1: Successful Project Completion
|
||||
**Given**:
|
||||
- All items in .ralph/fix_plan.md are marked [x]
|
||||
- Last test run shows all tests passing
|
||||
- No errors in recent logs/
|
||||
- All requirements from .ralph/specs/ are implemented
|
||||
|
||||
**When**: You evaluate project status at end of loop
|
||||
|
||||
**Then**: You must output:
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: COMPLETE
|
||||
TASKS_COMPLETED_THIS_LOOP: 1
|
||||
FILES_MODIFIED: 1
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: DOCUMENTATION
|
||||
EXIT_SIGNAL: true
|
||||
RECOMMENDATION: All requirements met, project ready for review
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Ralph's Action**: Detects EXIT_SIGNAL=true, gracefully exits loop with success message
|
||||
# ABE — Autonomous Bug Explorer
|
||||
## Instrucciones Maestras para Claude Code (via Ralph)
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Test-Only Loop Detected
|
||||
**Given**:
|
||||
- Last 3 loops only executed tests (npm test, bats, pytest, etc.)
|
||||
- No new files were created
|
||||
- No existing files were modified
|
||||
- No implementation work was performed
|
||||
## Visión del proyecto
|
||||
|
||||
**When**: You start a new loop iteration
|
||||
ABE es una plataforma enterprise self-hosted de descubrimiento autónomo de bugs
|
||||
en aplicaciones web. Explora apps como un usuario real, inyecta inputs inválidos
|
||||
(fuzzing), detecta anomalías, y genera bug reports reproducibles.
|
||||
|
||||
**Then**: You must output:
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS
|
||||
TASKS_COMPLETED_THIS_LOOP: 0
|
||||
FILES_MODIFIED: 0
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: TESTING
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: All tests passing, no implementation needed
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
**Posicionamiento**: "Playwright discovers what you test. ABE discovers what you miss."
|
||||
|
||||
**Ralph's Action**: Increments test_only_loops counter, exits after 3 consecutive test-only loops
|
||||
Modelo open-core enterprise self-hosted:
|
||||
- Free/OSS: exploración autónoma + reports básicos
|
||||
- Pro: dashboards avanzados, integraciones, CLI/CI
|
||||
- Enterprise: SSO, RBAC avanzado, LDAP, audit logs, licencia
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Stuck on Recurring Error
|
||||
**Given**:
|
||||
- Same error appears in last 5 consecutive loops
|
||||
- No progress on fixing the error
|
||||
- Error message is identical or very similar
|
||||
## Estado actual del código
|
||||
|
||||
**When**: You encounter the same error again
|
||||
|
||||
**Then**: You must output:
|
||||
Las fases 1-11 del proyecto original están implementadas con esta estructura:
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: 0
|
||||
FILES_MODIFIED: 2
|
||||
TESTS_STATUS: FAILING
|
||||
WORK_TYPE: DEBUGGING
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: Stuck on [error description] - human intervention needed
|
||||
---END_RALPH_STATUS---
|
||||
src/
|
||||
├── core/ ← interfaces.ts, ExplorationEngine, StateGraph, AnomalyDetector
|
||||
├── plugins/ ← PlaywrightAgent, collectors, exporters, fuzzers, reproducers
|
||||
├── server/ ← Express API server + socket.io
|
||||
├── db/ ← SQLite repositories (better-sqlite3)
|
||||
├── cli.ts ← CLI entry point
|
||||
frontend/ ← React + Vite + Tailwind (básico)
|
||||
```
|
||||
|
||||
**Ralph's Action**: Circuit breaker detects repeated errors, opens circuit after 5 loops
|
||||
El objetivo es REFACTORIZAR este código existente hacia una arquitectura
|
||||
modular hexagonal, migrando pieza por pieza sin romper funcionalidad.
|
||||
|
||||
**ESTRATEGIA DE MIGRACIÓN**: No reescribir de cero. Mover código existente
|
||||
a la nueva estructura, adaptar interfaces, y verificar que todo sigue funcionando
|
||||
después de cada movimiento.
|
||||
|
||||
---
|
||||
|
||||
### Scenario 4: No Work Remaining
|
||||
**Given**:
|
||||
- All tasks in fix_plan.md are complete
|
||||
- You analyze .ralph/specs/ and find nothing new to implement
|
||||
- Code quality is acceptable
|
||||
- Tests are passing
|
||||
## Arquitectura objetivo: Modular Monolith Hexagonal
|
||||
|
||||
**When**: You search for work to do and find none
|
||||
|
||||
**Then**: You must output:
|
||||
### Principio fundamental
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: COMPLETE
|
||||
TASKS_COMPLETED_THIS_LOOP: 0
|
||||
FILES_MODIFIED: 0
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: DOCUMENTATION
|
||||
EXIT_SIGNAL: true
|
||||
RECOMMENDATION: No remaining work, all .ralph/specs implemented
|
||||
---END_RALPH_STATUS---
|
||||
Infrastructure → Application → Domain
|
||||
(el código SIEMPRE apunta hacia adentro, nunca al revés)
|
||||
```
|
||||
|
||||
**Ralph's Action**: Detects completion signal, exits loop immediately
|
||||
### Estructura de carpetas OBJETIVO
|
||||
```
|
||||
src/
|
||||
├── shared/
|
||||
│ ├── domain/
|
||||
│ │ ├── Entity.ts
|
||||
│ │ ├── AggregateRoot.ts
|
||||
│ │ ├── ValueObject.ts
|
||||
│ │ ├── UniqueId.ts
|
||||
│ │ ├── Result.ts
|
||||
│ │ └── DomainEvent.ts
|
||||
│ ├── application/
|
||||
│ │ ├── UseCase.ts
|
||||
│ │ ├── EventBus.ts
|
||||
│ │ └── EventHandler.ts
|
||||
│ └── infrastructure/
|
||||
│ ├── InProcessEventBus.ts
|
||||
│ ├── DatabaseConnection.ts
|
||||
│ ├── Logger.ts
|
||||
│ ├── Config.ts
|
||||
│ └── StorageProvider.ts
|
||||
│
|
||||
├── modules/
|
||||
│ ├── crawling/
|
||||
│ │ ├── domain/ (entities, value-objects, events, ports)
|
||||
│ │ ├── application/ (commands, queries, event-handlers)
|
||||
│ │ └── infrastructure/(adapters, repositories, http)
|
||||
│ ├── fuzzing/ (misma estructura)
|
||||
│ ├── findings/ (misma estructura)
|
||||
│ ├── auth/ (misma estructura)
|
||||
│ ├── reporting/ (misma estructura)
|
||||
│ ├── integrations/ (misma estructura)
|
||||
│ ├── scheduling/ (misma estructura)
|
||||
│ └── licensing/ (misma estructura)
|
||||
│
|
||||
├── api/
|
||||
│ ├── server.ts
|
||||
│ ├── router.ts
|
||||
│ └── middleware/
|
||||
├── realtime/
|
||||
│ └── SocketGateway.ts
|
||||
├── jobs/
|
||||
│ ├── JobQueue.ts
|
||||
│ ├── SQLiteJobQueue.ts
|
||||
│ └── workers/
|
||||
├── cli/
|
||||
│ └── abe.ts
|
||||
└── main.ts ← composition root
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 5: Making Progress
|
||||
**Given**:
|
||||
- Tasks remain in .ralph/fix_plan.md
|
||||
- Implementation is underway
|
||||
- Files are being modified
|
||||
- Tests are passing or being fixed
|
||||
## Reglas de arquitectura INQUEBRANTABLES
|
||||
|
||||
**When**: You complete a task successfully
|
||||
|
||||
**Then**: You must output:
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: IN_PROGRESS
|
||||
TASKS_COMPLETED_THIS_LOOP: 3
|
||||
FILES_MODIFIED: 7
|
||||
TESTS_STATUS: PASSING
|
||||
WORK_TYPE: IMPLEMENTATION
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: Continue with next task from .ralph/fix_plan.md
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Ralph's Action**: Continues loop, circuit breaker stays CLOSED (normal operation)
|
||||
1. **Domain layer NO importa nada externo** — ni Kysely, ni Express, ni Playwright.
|
||||
2. **Cross-module communication SOLO via EventBus** — NUNCA import directo entre módulos.
|
||||
3. **Cada módulo exporta SOLO su facade** via `index.ts`.
|
||||
4. **Controllers son THIN** — parsean request, llaman use case, formatean response.
|
||||
5. **Use Cases retornan Result<T, E>** — NUNCA throw para errores de negocio.
|
||||
6. **Un archivo = una clase = una responsabilidad**.
|
||||
7. **Determinista** — no usar Math.random() sin seed. Loguear siempre el seed.
|
||||
8. **Serializable** — entities y value objects JSON.stringify-able.
|
||||
9. **No AI en el core loop** — AIEnrichment es post-proceso opcional.
|
||||
10. **Plugins nunca se importan desde core** — core solo define interfaces/ports.
|
||||
|
||||
---
|
||||
|
||||
### Scenario 6: Blocked on External Dependency
|
||||
**Given**:
|
||||
- Task requires external API, library, or human decision
|
||||
- Cannot proceed without missing information
|
||||
- Have tried reasonable workarounds
|
||||
## Stack tecnológico
|
||||
|
||||
**When**: You identify the blocker
|
||||
### Backend
|
||||
- Runtime: Node.js 20 + TypeScript 5.x (strict mode)
|
||||
- HTTP: Express.js 4.x
|
||||
- WebSocket: socket.io 4.x
|
||||
- Database: Kysely (query builder) + better-sqlite3 (default) | pg (enterprise)
|
||||
- Validation: Zod (schemas compartidos frontend/backend)
|
||||
- Auth: Better Auth + CASL
|
||||
- Browser: Playwright
|
||||
- Logger: Pino + pino-pretty (dev)
|
||||
- Jobs: SQLite-backed queue custom con worker_threads
|
||||
- Scheduler: node-cron
|
||||
- Security: Helmet, express-rate-limit, cors
|
||||
- API docs: zod-to-openapi + Scalar UI
|
||||
- Testing: Vitest + supertest (integration)
|
||||
|
||||
**Then**: You must output:
|
||||
```
|
||||
---RALPH_STATUS---
|
||||
STATUS: BLOCKED
|
||||
TASKS_COMPLETED_THIS_LOOP: 0
|
||||
FILES_MODIFIED: 0
|
||||
TESTS_STATUS: NOT_RUN
|
||||
WORK_TYPE: IMPLEMENTATION
|
||||
EXIT_SIGNAL: false
|
||||
RECOMMENDATION: Blocked on [specific dependency] - need [what's needed]
|
||||
---END_RALPH_STATUS---
|
||||
```
|
||||
|
||||
**Ralph's Action**: Logs blocker, may exit after multiple blocked loops
|
||||
### Frontend
|
||||
- React 18 + Vite + TypeScript
|
||||
- shadcn/ui (Radix UI + Tailwind CSS)
|
||||
- Tremor + Recharts (charts/dashboards)
|
||||
- TanStack Table + TanStack Query
|
||||
- Zustand (client state)
|
||||
- React Hook Form + Zod resolver
|
||||
- socket.io-client
|
||||
- Framer Motion
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
- .ralph/: Ralph-specific configuration and documentation
|
||||
- specs/: Project specifications and requirements
|
||||
- fix_plan.md: Prioritized TODO list
|
||||
- AGENT.md: Project build and run instructions
|
||||
- PROMPT.md: This file - Ralph development instructions
|
||||
- logs/: Loop execution logs
|
||||
- docs/generated/: Auto-generated documentation
|
||||
- src/: Source code implementation
|
||||
- examples/: Example usage and test cases
|
||||
## REGLAS OBLIGATORIAS PARA CADA TAREA
|
||||
|
||||
## Current Task
|
||||
Follow .ralph/fix_plan.md and choose the most important item to implement next.
|
||||
Use your judgment to prioritize what will have the biggest impact on project progress.
|
||||
### Antes de empezar
|
||||
1. Leer la tarea actual del fix_plan.md
|
||||
2. Leer la spec correspondiente en .ralph/specs/ SI existe
|
||||
3. Verificar que las dependencias (tareas previas) están completas
|
||||
|
||||
Remember: Quality over speed. Build it right the first time. Know when you're done.
|
||||
### Después de CADA tarea individual
|
||||
1. `npm run build` — DEBE compilar sin errores
|
||||
2. `cd frontend && npm run build` — DEBE compilar sin errores
|
||||
3. `npm run test` — DEBE pasar (o no romper tests existentes)
|
||||
4. Si ALGUNO falla → NO marcar tarea → arreglar PRIMERO
|
||||
|
||||
### Después de marcar tarea como completa
|
||||
1. `git add -A`
|
||||
2. `git commit -m "fase(X.Y): descripción breve de la tarea"`
|
||||
Ejemplo: `git commit -m "fase(1.3): create ValueObject base class with equals"`
|
||||
3. Verificar que el commit se hizo correctamente
|
||||
|
||||
### Reglas de código
|
||||
- Todo nuevo código DEBE tener tipos explícitos (CERO `any`)
|
||||
- Imports ordenados: node_modules → shared → modules → relative
|
||||
- Nombres: PascalCase para clases, camelCase para funciones/variables
|
||||
- Cada módulo nuevo DEBE tener al menos un test unitario
|
||||
- Código existente que se MUEVE debe seguir funcionando igual
|
||||
|
||||
---
|
||||
|
||||
## Señal de completado
|
||||
|
||||
Cuando TODAS las tareas en fix_plan.md estén marcadas [x]:
|
||||
|
||||
RALPH_STATUS:
|
||||
completion_indicators: done
|
||||
EXIT_SIGNAL: true
|
||||
summary: "ABE enterprise refactor complete."
|
||||
|
||||
@@ -1,27 +1,444 @@
|
||||
# Ralph Fix Plan
|
||||
# ABE Enterprise Refactor — Fix Plan
|
||||
|
||||
## High Priority
|
||||
- [ ] Set up basic project structure and build system
|
||||
- [ ] Define core data structures and types
|
||||
- [ ] Implement basic input/output handling
|
||||
- [ ] Create test framework and initial tests
|
||||
## REGLAS CRÍTICAS
|
||||
1. NO pasar a la siguiente tarea si el build falla
|
||||
2. Hacer `git commit` después de CADA tarea completada
|
||||
3. Leer la spec en `.ralph/specs/` ANTES de cada phase
|
||||
4. Los tests DEBEN pasar antes de marcar [x]
|
||||
5. Formato commit: `git commit -m "fase(X.Y): descripción"`
|
||||
|
||||
## Medium Priority
|
||||
- [ ] Add error handling and validation
|
||||
- [ ] Implement core business logic
|
||||
- [ ] Add configuration management
|
||||
- [ ] Create user documentation
|
||||
---
|
||||
|
||||
## Low Priority
|
||||
- [ ] Performance optimization
|
||||
- [ ] Extended feature set
|
||||
- [ ] Integration with external services
|
||||
- [ ] Advanced error recovery
|
||||
## Phase 0: Hotfix — Build actual funcional [PENDIENTE]
|
||||
|
||||
## Completed
|
||||
- [x] Project initialization
|
||||
- [ ] 0.1: Fix errores TypeScript en src/ que impidan compilación (IAnomaly import, NodeListOf iterator, cualquier otro)
|
||||
- [ ] 0.2: Verificar `npm run build` pasa con 0 errores
|
||||
- [ ] 0.3: Verificar `cd frontend && npm run build` pasa con 0 errores
|
||||
- [ ] 0.4: Verificar que la app arranca con `npm run dev` sin crash
|
||||
- [ ] 0.5: Commit: `git add -A && git commit -m "fase(0): fix build errors"`
|
||||
|
||||
## Notes
|
||||
- Focus on MVP functionality first
|
||||
- Ensure each feature is properly tested
|
||||
- Update this file after each major milestone
|
||||
---
|
||||
|
||||
## Phase 1: Shared Domain — Building Blocks [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-01-shared-domain.md`
|
||||
|
||||
- [ ] 1.1: Crear directorio `src/shared/domain/`
|
||||
- [ ] 1.2: Crear `src/shared/domain/Result.ts` — Result<T, E> con Ok(), Err(), isOk(), isErr()
|
||||
- [ ] 1.3: Crear `src/shared/domain/UniqueId.ts` — UUID v4 wrapper con create(), toString(), equals()
|
||||
- [ ] 1.4: Crear `src/shared/domain/Entity.ts` — base class con _id: UniqueId, equals()
|
||||
- [ ] 1.5: Crear `src/shared/domain/AggregateRoot.ts` — extends Entity + domainEvents[], addDomainEvent(), clearEvents()
|
||||
- [ ] 1.6: Crear `src/shared/domain/ValueObject.ts` — base class inmutable con props frozen, equals()
|
||||
- [ ] 1.7: Crear `src/shared/domain/DomainEvent.ts` — interface: eventId, eventName, aggregateId, occurredOn, payload
|
||||
- [ ] 1.8: Crear `src/shared/application/UseCase.ts` — interface: execute(req) → Promise<Result<TRes, TErr>>
|
||||
- [ ] 1.9: Crear `src/shared/application/EventBus.ts` — interface: publish(event), subscribe(name, handler)
|
||||
- [ ] 1.10: Crear `src/shared/application/EventHandler.ts` — interface: handle(event) → Promise<void>
|
||||
- [ ] 1.11: Crear `src/shared/domain/index.ts` — barrel export de todo shared/domain
|
||||
- [ ] 1.12: Crear `src/shared/application/index.ts` — barrel export de todo shared/application
|
||||
- [ ] 1.13: Tests unitarios: Result (Ok/Err/isOk/isErr), Entity (equals by id), ValueObject (equals by props), UniqueId (create/equals)
|
||||
- [ ] 1.14: Verificar build completo + commit: `fase(1): shared domain building blocks`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Shared Infrastructure [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-02-shared-infrastructure.md`
|
||||
|
||||
- [ ] 2.1: Instalar deps: `npm i kysely better-sqlite3 pino pino-pretty zod helmet express-rate-limit dotenv uuid` + `npm i -D @types/better-sqlite3 @types/uuid`
|
||||
- [ ] 2.2: Crear `src/shared/infrastructure/Config.ts` — Zod schema para TODAS las env vars con defaults sensatos
|
||||
- [ ] 2.3: Crear `src/shared/infrastructure/Logger.ts` — Pino factory: createLogger(config) retorna pino.Logger, pino-pretty en dev
|
||||
- [ ] 2.4: Crear `src/shared/infrastructure/DatabaseConnection.ts` — Kysely factory: createDatabase(config) soporta SQLite (default) y PostgreSQL (si config.db.driver === 'postgres')
|
||||
- [ ] 2.5: Crear `src/shared/infrastructure/InProcessEventBus.ts` — implementa EventBus con Node EventEmitter, logging de eventos, error handling en handlers
|
||||
- [ ] 2.6: Crear `src/shared/infrastructure/StorageProvider.ts` — interface IStorageProvider (save/get/delete/exists) + LocalStorageProvider (filesystem)
|
||||
- [ ] 2.7: Crear `src/shared/infrastructure/index.ts` — barrel export
|
||||
- [ ] 2.8: Crear `src/db/migrations/001_initial_schema.ts` — migración Kysely que crea las tablas existentes (sessions, states, actions, anomalies, notifications) con IF NOT EXISTS
|
||||
- [ ] 2.9: Crear `src/db/migrator.ts` — setup Kysely Migrator + función runMigrations()
|
||||
- [ ] 2.10: Añadir script `"db:migrate"` a package.json
|
||||
- [ ] 2.11: Tests: Config validation (valid + invalid), EventBus (publish/subscribe/error handling), StorageProvider (save/get/delete)
|
||||
- [ ] 2.12: Verificar build completo + commit: `fase(2): shared infrastructure layer`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Crawling Module — Domain + Application [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-03-crawling-domain.md`
|
||||
|
||||
- [ ] 3.1: Crear `src/modules/crawling/domain/entities/CrawlSession.ts` — AggregateRoot con url, status, seed, maxStates, statesVisited, config
|
||||
- [ ] 3.2: Crear `src/modules/crawling/domain/entities/CrawlState.ts` — Entity con url, title, domSnapshot, visitCount
|
||||
- [ ] 3.3: Crear `src/modules/crawling/domain/entities/CrawlAction.ts` — Entity con type, selector, value, seed, stateId, sequenceOrder
|
||||
- [ ] 3.4: Crear value objects: `Url.ts`, `Selector.ts`, `SessionStatus.ts` (running/completed/failed/stopped)
|
||||
- [ ] 3.5: Crear events: `CrawlStarted.ts`, `StateDiscovered.ts`, `ActionExecuted.ts`, `CrawlCompleted.ts`, `CrawlFailed.ts`
|
||||
- [ ] 3.6: Crear ports: `ICrawlerEngine.ts` (launch/close/discoverActions/executeAction/captureState), `ICrawlSessionRepository.ts` (save/findById/findAll/update), `IStateRepository.ts`
|
||||
- [ ] 3.7: Crear `application/commands/StartCrawlCommand.ts` — use case que valida config, crea CrawlSession, emite CrawlStarted
|
||||
- [ ] 3.8: Crear `application/commands/StopCrawlCommand.ts` — use case que para sesión, emite CrawlCompleted
|
||||
- [ ] 3.9: Crear `application/queries/GetSessionQuery.ts` y `ListSessionsQuery.ts`
|
||||
- [ ] 3.10: Crear `modules/crawling/index.ts` — barrel export público
|
||||
- [ ] 3.11: Tests: CrawlSession creation + domain events, StartCrawlCommand con mock repository
|
||||
- [ ] 3.12: Verificar build + commit: `fase(3): crawling module domain and application`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Crawling Module — Infrastructure (migración código existente) [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-04-crawling-infrastructure.md`
|
||||
|
||||
- [ ] 4.1: Copiar `src/plugins/agents/PlaywrightAgent.ts` → `src/modules/crawling/infrastructure/adapters/PlaywrightCrawlerEngine.ts`, adaptar para implementar ICrawlerEngine port
|
||||
- [ ] 4.2: Copiar `src/core/StateGraph.ts` → `src/modules/crawling/infrastructure/adapters/StateGraph.ts`, mantener lógica BFS
|
||||
- [ ] 4.3: Copiar `src/core/ExplorationEngine.ts` → `src/modules/crawling/infrastructure/adapters/ExplorationOrchestrator.ts`, adaptar para usar ports en vez de imports directos
|
||||
- [ ] 4.4: Crear `infrastructure/repositories/KyselyCrawlSessionRepository.ts` — implementa ICrawlSessionRepository con Kysely
|
||||
- [ ] 4.5: Crear `infrastructure/repositories/KyselyStateRepository.ts`
|
||||
- [ ] 4.6: Crear `infrastructure/http/CrawlingController.ts` — Express routes: POST /api/sessions, GET /api/sessions, GET /api/sessions/:id, DELETE /api/sessions/:id
|
||||
- [ ] 4.7: Verificar que crear sesión + ejecutar crawl funciona end-to-end
|
||||
- [ ] 4.8: Verificar build + commit: `fase(4): crawling infrastructure with migrated code`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Findings Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-05-findings-module.md`
|
||||
|
||||
- [ ] 5.1: Crear `domain/entities/Finding.ts` — AggregateRoot con severity, type, evidence, status, actionTrace
|
||||
- [ ] 5.2: Crear value objects: `Severity.ts` (low/medium/high/critical), `FindingType.ts`, `Evidence.ts`, `FindingStatus.ts` (open/investigating/resolved/closed)
|
||||
- [ ] 5.3: Crear events: `FindingCreated.ts`, `FindingResolved.ts`, `FindingEnriched.ts`
|
||||
- [ ] 5.4: Crear ports: `IFindingRepository.ts`, `IAIEnricher.ts`
|
||||
- [ ] 5.5: Crear commands: `CreateFindingCommand.ts`, `EnrichFindingCommand.ts`, `ResolveFindingCommand.ts`
|
||||
- [ ] 5.6: Crear queries: `GetFindingQuery.ts`, `ListFindingsQuery.ts` (filtros: severity, type, session, status, search), `FindingStatsQuery.ts`
|
||||
- [ ] 5.7: Crear `event-handlers/OnAnomalyDetected.ts` — escucha eventos crawling → crea Finding
|
||||
- [ ] 5.8: Crear `infrastructure/repositories/KyselyFindingRepository.ts`
|
||||
- [ ] 5.9: Migrar exporters existentes → `infrastructure/exporters/` (MarkdownExporter, JSONExporter)
|
||||
- [ ] 5.10: Crear `infrastructure/exporters/PlaywrightScriptExporter.ts` — genera test Playwright reproducible desde actionTrace
|
||||
- [ ] 5.11: Crear `infrastructure/http/FindingsController.ts` — routes para anomalies existentes + nuevas
|
||||
- [ ] 5.12: Migración Kysely: tabla findings con columnas status, browser, ai_enrichment_json
|
||||
- [ ] 5.13: Tests: Finding aggregate, CreateFinding, ListFindings con filtros
|
||||
- [ ] 5.14: Verificar build + commit: `fase(5): findings module complete`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Fuzzing Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-06-fuzzing-module.md`
|
||||
|
||||
- [ ] 6.1: Crear domain: `FuzzSession.ts` (AggregateRoot), `FuzzResult.ts` (Entity)
|
||||
- [ ] 6.2: Crear value objects: `FuzzStrategy.ts`, `FuzzPayload.ts`, `Seed.ts`, `FuzzIntensity.ts`
|
||||
- [ ] 6.3: Crear events: `FuzzStarted.ts`, `VulnerabilityDetected.ts`, `FuzzCompleted.ts`
|
||||
- [ ] 6.4: Crear port: `IFuzzerEngine.ts`
|
||||
- [ ] 6.5: Crear `commands/RunFuzzCommand.ts`
|
||||
- [ ] 6.6: Crear `event-handlers/OnActionExecuted.ts` — escucha crawling → trigger fuzzing
|
||||
- [ ] 6.7: Migrar las 5 estrategias existentes → `infrastructure/strategies/` (Empty, Oversized, SpecialChars, TypeMismatch, Boundary)
|
||||
- [ ] 6.8: Migrar `FuzzingEngine.ts` y `InputTypeDetector.ts` → `infrastructure/adapters/`
|
||||
- [ ] 6.9: Crear `infrastructure/http/FuzzingController.ts`
|
||||
- [ ] 6.10: Tests: cada estrategia de fuzzing genera payloads válidos
|
||||
- [ ] 6.11: Verificar build + commit: `fase(6): fuzzing module complete`
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: API Server Refactor + Composition Root [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-07-api-server.md`
|
||||
|
||||
- [ ] 7.1: Crear `src/api/middleware/errorHandler.ts` — AppError hierarchy (ValidationError, AuthenticationError, ForbiddenError, NotFoundError) + global error handler
|
||||
- [ ] 7.2: Crear `src/api/middleware/requestId.ts` — genera UUID por request, adjunta a req + pino child logger
|
||||
- [ ] 7.3: Crear `src/api/middleware/notFound.ts` — 404 handler para rutas no encontradas
|
||||
- [ ] 7.4: Crear `src/api/server.ts` — Express app con middleware stack: requestId → helmet → cors → rateLimit → bodyParser → routes → notFound → errorHandler
|
||||
- [ ] 7.5: Crear `src/api/router.ts` — registra routes de TODOS los módulos (crawling, findings, fuzzing)
|
||||
- [ ] 7.6: Crear `src/realtime/SocketGateway.ts` — socket.io server que subscribe a EventBus y emite a clientes
|
||||
- [ ] 7.7: Crear `src/main.ts` — composition root: load config → create logger → create db → run migrations → create event bus → create repositories → create use cases → subscribe handlers → create controllers → create Express → create socket.io → start listening
|
||||
- [ ] 7.8: Implementar graceful shutdown en main.ts: SIGTERM/SIGINT → stop accepting → close sockets → close db → flush logs → exit
|
||||
- [ ] 7.9: Health endpoints: GET /health/live (process alive), GET /health/ready (DB check)
|
||||
- [ ] 7.10: Verificar que TODOS los endpoints existentes siguen funcionando tras refactor
|
||||
- [ ] 7.11: Verificar build + commit: `fase(7): api server refactor with composition root`
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Job Queue System [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-08-job-queue.md`
|
||||
|
||||
- [ ] 8.1: Crear `src/jobs/JobQueue.ts` — interface: enqueue, start, pause, waitForActive
|
||||
- [ ] 8.2: Crear `src/jobs/SQLiteJobQueue.ts` — tabla jobs con status/type/payload/attempts/run_at, polling worker
|
||||
- [ ] 8.3: Migración Kysely: tabla jobs
|
||||
- [ ] 8.4: Crear `src/jobs/workers/ExplorationWorker.ts` — ejecuta crawl como job
|
||||
- [ ] 8.5: Crear `src/jobs/workers/ReportWorker.ts` — genera reports en background
|
||||
- [ ] 8.6: Integrar job queue en main.ts, mover exploraciones de sync a job-based
|
||||
- [ ] 8.7: Tests: enqueue → dequeue → complete cycle, failed job retry
|
||||
- [ ] 8.8: Verificar build + commit: `fase(8): sqlite job queue system`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Auth Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-09-auth-module.md`
|
||||
|
||||
- [ ] 9.1: Instalar: `npm i better-auth @casl/ability argon2`
|
||||
- [ ] 9.2: Crear domain: `User.ts` (AggregateRoot), `Organization.ts` (AggregateRoot), `Team.ts` (Entity), `ApiKey.ts` (Entity)
|
||||
- [ ] 9.3: Crear value objects: `Email.ts`, `Role.ts` (owner/admin/member/viewer), `Permission.ts`
|
||||
- [ ] 9.4: Crear events: `UserCreated.ts`, `UserLoggedIn.ts`, `OrgCreated.ts`, `MemberInvited.ts`
|
||||
- [ ] 9.5: Crear ports: `IUserRepository.ts`, `IOrganizationRepository.ts`
|
||||
- [ ] 9.6: Crear commands: `RegisterCommand.ts`, `LoginCommand.ts`, `CreateOrganizationCommand.ts`, `InviteMemberCommand.ts`, `CreateApiKeyCommand.ts`
|
||||
- [ ] 9.7: Crear queries: `GetUserQuery.ts`, `ListOrgMembersQuery.ts`
|
||||
- [ ] 9.8: Crear `infrastructure/better-auth/authConfig.ts` — setup Better Auth con SQLite adapter, email+password, organization plugin con roles
|
||||
- [ ] 9.9: Crear `infrastructure/casl/AbilityFactory.ts` — define permisos por role (owner: manage all, admin: manage all except delete org, member: create/read sessions+findings, viewer: read all)
|
||||
- [ ] 9.10: Crear `application/middleware/AuthMiddleware.ts` — intenta session cookie → JWT → API key → 401
|
||||
- [ ] 9.11: Crear `application/middleware/RBACMiddleware.ts` — verifica permisos CASL por ruta
|
||||
- [ ] 9.12: Crear `infrastructure/repositories/KyselyUserRepository.ts`
|
||||
- [ ] 9.13: Crear `infrastructure/http/AuthController.ts` — POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me, GET /api/auth/setup-required
|
||||
- [ ] 9.14: Migración Kysely: tablas users, organizations, teams, org_members, api_keys, auth_sessions
|
||||
- [ ] 9.15: First-run detection: si 0 users → GET /api/auth/setup-required retorna { required: true }
|
||||
- [ ] 9.16: POST /api/auth/setup — crea primer user como owner + organización default
|
||||
- [ ] 9.17: Integrar AuthMiddleware en todas las rutas /api/ excepto /health/* y /api/auth/*
|
||||
- [ ] 9.18: Tests: register, login, RBAC permissions (admin can create session, viewer cannot)
|
||||
- [ ] 9.19: Verificar build + commit: `fase(9): auth module with better-auth and casl`
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Frontend — shadcn/ui Shell [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-10-frontend-shell.md`
|
||||
|
||||
- [ ] 10.1: En frontend/: instalar shadcn/ui con `npx shadcn@latest init` (Vite, Zinc, CSS variables, Tailwind)
|
||||
- [ ] 10.2: Instalar componentes shadcn: button, input, card, badge, dialog, dropdown-menu, command, sidebar, tabs, table, toast, form, separator, avatar, skeleton, tooltip, sheet, select, textarea, label, switch, alert
|
||||
- [ ] 10.3: Instalar deps: `npm i @tanstack/react-query @tanstack/react-table zustand react-hook-form @hookform/resolvers framer-motion react-hotkeys-hook`
|
||||
- [ ] 10.4: Crear layout: `components/layout/AppSidebar.tsx` — sidebar collapsible con nav items (Dashboard, Explorations, Findings, Reports, Settings)
|
||||
- [ ] 10.5: Crear `components/layout/TopBar.tsx` — logo, search trigger (⌘K), theme toggle, user avatar menu
|
||||
- [ ] 10.6: Crear `components/layout/AppLayout.tsx` — wrapper: Sidebar + TopBar + Content outlet
|
||||
- [ ] 10.7: Crear `components/layout/CommandPalette.tsx` — ⌘K con shadcn Command component
|
||||
- [ ] 10.8: Crear ThemeProvider: dark mode como default, toggle dark/light, persistir en localStorage
|
||||
- [ ] 10.9: Crear `lib/api.ts` — API client con fetch, credentials: include, auto-redirect a /login en 401
|
||||
- [ ] 10.10: Crear `lib/queryClient.ts` — TanStack Query provider
|
||||
- [ ] 10.11: Crear `stores/uiStore.ts` — Zustand: sidebarCollapsed, theme
|
||||
- [ ] 10.12: Crear pages/Login.tsx — form email + password con shadcn
|
||||
- [ ] 10.13: Crear pages/Setup.tsx — wizard first-run (crear admin + nombre org)
|
||||
- [ ] 10.14: Crear `components/layout/ProtectedRoute.tsx` — check auth, redirect a /login o /setup
|
||||
- [ ] 10.15: Actualizar App.tsx con React Router: / (dashboard), /login, /setup, /sessions/:id, /findings/:id, /settings — todo wrapped en ProtectedRoute excepto login/setup
|
||||
- [ ] 10.16: Verificar frontend build + commit: `fase(10): frontend shadcn-ui shell with auth`
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Dashboard Page [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-11-dashboard.md`
|
||||
|
||||
- [ ] 11.1: Instalar en frontend: `npm i tremor recharts`
|
||||
- [ ] 11.2: Crear `hooks/useFindings.ts` — TanStack Query hooks: useFindings, useFindingStats
|
||||
- [ ] 11.3: Crear `hooks/useSessions.ts` — TanStack Query hooks: useSessions, useSession
|
||||
- [ ] 11.4: Crear `hooks/useSocket.ts` — socket.io-client connection con auto-reconnect
|
||||
- [ ] 11.5: Crear `components/dashboard/KPICards.tsx` — 4 cards Tremor: Total Findings, Critical/High, Active Sessions, Coverage
|
||||
- [ ] 11.6: Crear `components/dashboard/TrendChart.tsx` — Recharts AreaChart stacked por severity, últimos 30 días
|
||||
- [ ] 11.7: Crear `components/dashboard/SeverityDistribution.tsx` — Recharts PieChart con colores por severity
|
||||
- [ ] 11.8: Crear `components/dashboard/RecentFindings.tsx` — TanStack Table, 10 rows, click → /findings/:id
|
||||
- [ ] 11.9: Crear `components/dashboard/ActiveSessions.tsx` — lista con progress bars, click → /sessions/:id
|
||||
- [ ] 11.10: Crear `components/dashboard/QuickActions.tsx` — botón "New Exploration" prominente
|
||||
- [ ] 11.11: Crear `pages/Dashboard.tsx` — ensambla todo, responsive 2col desktop 1col mobile
|
||||
- [ ] 11.12: Conectar real-time: socket events actualizan KPIs y recent findings
|
||||
- [ ] 11.13: Verificar frontend build + commit: `fase(11): dashboard page with charts and realtime`
|
||||
|
||||
---
|
||||
|
||||
## Phase 12: Sessions Pages [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-12-sessions-pages.md`
|
||||
|
||||
- [ ] 12.1: Crear `components/sessions/NewExplorationForm.tsx` — React Hook Form + Zod: URL, seed, maxStates, maxDepth, allowedDomains (chips), excludedPaths (chips), auth type (none/cookies/headers/login_flow) con campos condicionales, fuzzing toggle + intensity, collapsible advanced section
|
||||
- [ ] 12.2: Crear `pages/sessions/SessionList.tsx` — TanStack Table: status badge, url, findings count, duration, created at; sortable + filterable
|
||||
- [ ] 12.3: Crear `pages/sessions/SessionDetail.tsx` — layout con tabs
|
||||
- [ ] 12.4: Crear `components/sessions/LiveFeed.tsx` — streaming WebSocket con auto-scroll, colores por event type (verde state, amarillo action, rojo anomaly)
|
||||
- [ ] 12.5: Crear `components/sessions/SessionFindings.tsx` — findings de esta sesión con severity badges
|
||||
- [ ] 12.6: Crear `components/sessions/SessionConfig.tsx` — ExplorationConfig read-only
|
||||
- [ ] 12.7: Progress bar estados explorados / maxStates
|
||||
- [ ] 12.8: Stop button funcional (DELETE /api/sessions/:id)
|
||||
- [ ] 12.9: Verificar frontend build + commit: `fase(12): session pages with live feed`
|
||||
|
||||
---
|
||||
|
||||
## Phase 13: Findings Pages [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-13-findings-pages.md`
|
||||
|
||||
- [ ] 13.1: Crear `pages/findings/FindingsList.tsx` — TanStack Table con filtros: severity multi-select, type multi-select, status, session dropdown, text search
|
||||
- [ ] 13.2: Crear `pages/findings/FindingDetail.tsx` — split layout
|
||||
- [ ] 13.3: Crear `components/findings/ReproductionSteps.tsx` — numbered step cards con action type, selector, screenshot thumb
|
||||
- [ ] 13.4: Crear `components/findings/EvidencePanel.tsx` — tabs: Console (syntax-highlighted), Network (request/response table), DOM (snapshot viewer)
|
||||
- [ ] 13.5: Crear `components/findings/AIAnalysisPanel.tsx` — muestra enrichment si existe, o botón "Analyze with AI"
|
||||
- [ ] 13.6: Export buttons: "Export as Playwright", "Export as Markdown", "Export as JSON"
|
||||
- [ ] 13.7: Status workflow buttons: open → investigating → resolved → closed
|
||||
- [ ] 13.8: `components/common/SeverityBadge.tsx` — reutilizable con colores critical=rojo, high=naranja, medium=amarillo, low=azul
|
||||
- [ ] 13.9: Verificar frontend build + commit: `fase(13): findings pages with detail view`
|
||||
|
||||
---
|
||||
|
||||
## Phase 14: Settings Pages [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-14-settings-pages.md`
|
||||
|
||||
- [ ] 14.1: Crear `pages/settings/SettingsLayout.tsx` — layout con sidebar navigation entre sections
|
||||
- [ ] 14.2: Section "Profile" — cambiar nombre, email, password
|
||||
- [ ] 14.3: Section "Organization" — nombre org, invitar miembros, manage roles
|
||||
- [ ] 14.4: Section "API Keys" — crear (con nombre + permisos), listar, revocar
|
||||
- [ ] 14.5: Section "Exploration Defaults" — form con defaults para nuevas exploraciones
|
||||
- [ ] 14.6: Section "Notifications" — Slack webhook URL, min severity
|
||||
- [ ] 14.7: Section "Appearance" — tema dark/light, accent color
|
||||
- [ ] 14.8: Section "License" — ver status licencia, input para activar key
|
||||
- [ ] 14.9: Verificar frontend build + commit: `fase(14): settings pages`
|
||||
|
||||
---
|
||||
|
||||
## Phase 15: Reporting Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-15-reporting.md`
|
||||
|
||||
- [ ] 15.1: Crear domain: `Report.ts` (AggregateRoot), value objects `ReportFormat.ts` (pdf/html/json), `DateRange.ts`
|
||||
- [ ] 15.2: Crear port: `IReportGenerator.ts`
|
||||
- [ ] 15.3: Crear `commands/GenerateReportCommand.ts` — crea report con findings de un rango de fechas/sesión
|
||||
- [ ] 15.4: Crear `infrastructure/generators/HTMLReportGenerator.ts` — genera HTML report completo
|
||||
- [ ] 15.5: Crear `infrastructure/generators/PDFReportGenerator.ts` — usa Playwright para renderizar HTML → PDF
|
||||
- [ ] 15.6: Crear `infrastructure/http/ReportingController.ts` — POST /api/reports, GET /api/reports, GET /api/reports/:id/download
|
||||
- [ ] 15.7: Integrar con job queue: generación async
|
||||
- [ ] 15.8: Migración Kysely: tabla reports
|
||||
- [ ] 15.9: Frontend: `pages/Reports.tsx` — generar (dialog con filtros), listar, descargar
|
||||
- [ ] 15.10: Tests: GenerateReportCommand con mock generator
|
||||
- [ ] 15.11: Verificar build completo + commit: `fase(15): reporting module with pdf generation`
|
||||
|
||||
---
|
||||
|
||||
## Phase 16: Integrations Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-16-integrations.md`
|
||||
|
||||
- [ ] 16.1: Instalar: `npm i @slack/web-api @octokit/rest`
|
||||
- [ ] 16.2: Crear domain: `Integration.ts` (Entity), `WebhookEndpoint.ts` (Entity)
|
||||
- [ ] 16.3: Crear value objects: `IntegrationType.ts` (jira/slack/github/webhook), `WebhookSecret.ts`
|
||||
- [ ] 16.4: Crear port: `IIntegrationProvider.ts` (sendFinding)
|
||||
- [ ] 16.5: Crear `infrastructure/webhooks/WebhookDispatcher.ts` — HMAC-SHA256 signature, retry con exponential backoff (3 intentos)
|
||||
- [ ] 16.6: Crear `infrastructure/providers/SlackProvider.ts` — Block Kit message con severity, description, link
|
||||
- [ ] 16.7: Crear `infrastructure/providers/GitHubIssuesProvider.ts` — crea issue con reproduction steps
|
||||
- [ ] 16.8: Crear `infrastructure/providers/JiraProvider.ts` — REST API v3, crea issue con screenshots
|
||||
- [ ] 16.9: Crear `event-handlers/OnFindingCreated.ts` — dispatch a todas las integrations activas
|
||||
- [ ] 16.10: Crear `infrastructure/http/IntegrationsController.ts` — CRUD integrations + webhooks
|
||||
- [ ] 16.11: Migración Kysely: tables integrations, webhook_endpoints, webhook_deliveries
|
||||
- [ ] 16.12: Frontend: Settings/Integrations con forms por provider (Slack webhook URL, Jira config, GitHub token, custom webhook)
|
||||
- [ ] 16.13: Tests: webhook dispatch + HMAC verification
|
||||
- [ ] 16.14: Verificar build completo + commit: `fase(16): integrations module`
|
||||
|
||||
---
|
||||
|
||||
## Phase 17: Licensing Module [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-17-licensing.md`
|
||||
|
||||
- [ ] 17.1: Crear domain: `License.ts` (Entity), value objects `LicensePlan.ts` (free/pro/enterprise), `FeatureEntitlement.ts`
|
||||
- [ ] 17.2: Crear port: `ILicenseValidator.ts` (validate, getEntitlements)
|
||||
- [ ] 17.3: Crear `infrastructure/RSALicenseValidator.ts` — verifica firma RSA-2048 con public key bundled
|
||||
- [ ] 17.4: Crear feature flags: `FREE_FEATURES`, `PRO_FEATURES`, `ENTERPRISE_FEATURES` arrays
|
||||
- [ ] 17.5: Crear `infrastructure/middleware/FeatureGateMiddleware.ts` — checkea feature en license antes de permitir request
|
||||
- [ ] 17.6: Crear `infrastructure/http/LicensingController.ts` — POST /api/license/activate, GET /api/license/status
|
||||
- [ ] 17.7: Crear `scripts/generate-license.ts` — CLI tool para generar license keys firmadas (uso interno)
|
||||
- [ ] 17.8: Integrar gate checks en rutas Pro/Enterprise (reporting, integrations, etc.)
|
||||
- [ ] 17.9: Frontend: License section en Settings
|
||||
- [ ] 17.10: Tests: valid license passes, expired fails, wrong signature fails, feature gate blocks
|
||||
- [ ] 17.11: Verificar build completo + commit: `fase(17): licensing module with RSA validation`
|
||||
|
||||
---
|
||||
|
||||
## Phase 18: CLI + CI/CD [PENDIENTE]
|
||||
Spec: `.ralph/specs/phase-18-cli-cicd.md`
|
||||
|
||||
- [ ] 18.1: Instalar: `npm i commander`
|
||||
- [ ] 18.2: Refactorizar `src/cli/abe.ts` con commander: comando `explore` con flags --url, --config (json file), --output (json|junit|markdown), --fail-on-severity, --api-key
|
||||
- [ ] 18.3: Comando `abe report` — genera report de una sesión por id
|
||||
- [ ] 18.4: Comando `abe status` — ping al servidor, muestra sessions activas
|
||||
- [ ] 18.5: Output JUnit XML: cada finding = failing test, cada state sin findings = passing test
|
||||
- [ ] 18.6: Exit codes: 0=clean, 1=findings over threshold, 2=error
|
||||
- [ ] 18.7: Crear `.github/actions/abe-explore/action.yml` — GitHub Action composite
|
||||
- [ ] 18.8: Crear `Dockerfile.ci` — imagen con Chromium para CI (basada en mcr.microsoft.com/playwright)
|
||||
- [ ] 18.9: Crear `.github/workflows/abe-example.yml` — ejemplo completo
|
||||
- [ ] 18.10: Actualizar README.md con sección CLI
|
||||
- [ ] 18.11: Verificar build completo + commit: `fase(18): cli and cicd integration`
|
||||
|
||||
---
|
||||
|
||||
## Phase 19: Scheduling Module Refactor [PENDIENTE]
|
||||
|
||||
- [ ] 19.1: Migrar scheduling existente → nueva estructura modular (domain/application/infrastructure)
|
||||
- [ ] 19.2: Crear Schedule aggregate con cron validation (Zod)
|
||||
- [ ] 19.3: Integrar con job queue
|
||||
- [ ] 19.4: Crear SchedulingController con CRUD + toggle
|
||||
- [ ] 19.5: Frontend: Schedules section en Settings
|
||||
- [ ] 19.6: Verificar build + commit: `fase(19): scheduling module refactor`
|
||||
|
||||
---
|
||||
|
||||
## Phase 20: Visual Regression Refactor [PENDIENTE]
|
||||
|
||||
- [ ] 20.1: Migrar visual regression existente → nueva estructura modular
|
||||
- [ ] 20.2: Integrar con StorageProvider para screenshots
|
||||
- [ ] 20.3: Refactorizar frontend /visual-review con shadcn/ui components
|
||||
- [ ] 20.4: Verificar build + commit: `fase(20): visual regression refactor`
|
||||
|
||||
---
|
||||
|
||||
## Phase 21: API Documentation [PENDIENTE]
|
||||
|
||||
- [ ] 21.1: Instalar: `npm i @asteasolutions/zod-to-openapi @scalar/express-api-reference`
|
||||
- [ ] 21.2: Crear Zod schemas compartidos para TODOS los endpoints (request + response)
|
||||
- [ ] 21.3: Generar OpenAPI 3.1 spec desde Zod schemas
|
||||
- [ ] 21.4: Montar Scalar UI en GET /api-docs
|
||||
- [ ] 21.5: Servir spec JSON en GET /api-docs/openapi.json
|
||||
- [ ] 21.6: Verificar que todos los endpoints están documentados
|
||||
- [ ] 21.7: Verificar build + commit: `fase(21): openapi documentation with scalar`
|
||||
|
||||
---
|
||||
|
||||
## Phase 22: Docker Production [PENDIENTE]
|
||||
|
||||
- [ ] 22.1: Refactorizar Dockerfile backend: multi-stage, node:20-alpine, tini como init, non-root user, HEALTHCHECK
|
||||
- [ ] 22.2: Refactorizar frontend Dockerfile: multi-stage build + nginx
|
||||
- [ ] 22.3: Actualizar docker-compose.yml: healthcheck, restart policies, volumes, env_file
|
||||
- [ ] 22.4: Crear docker-compose.prod.yml
|
||||
- [ ] 22.5: Crear .dockerignore optimizado
|
||||
- [ ] 22.6: CMD DEBE ser `["tini", "--", "node", "dist/main.js"]` — NUNCA npm
|
||||
- [ ] 22.7: Verificar imagen final < 200MB
|
||||
- [ ] 22.8: Verificar docker compose up funciona end-to-end
|
||||
- [ ] 22.9: Commit: `fase(22): docker production setup`
|
||||
|
||||
---
|
||||
|
||||
## Phase 23: Observability [PENDIENTE]
|
||||
|
||||
- [ ] 23.1: Request correlation: requestId en CADA log entry via pino child logger
|
||||
- [ ] 23.2: Structured error logging con contexto (userId, sessionId, etc.)
|
||||
- [ ] 23.3: Liveness probe: GET /health/live
|
||||
- [ ] 23.4: Readiness probe: GET /health/ready (DB + job queue check)
|
||||
- [ ] 23.5: Startup probe: medir tiempo de arranque, loguear
|
||||
- [ ] 23.6: Commit: `fase(23): observability and health probes`
|
||||
|
||||
---
|
||||
|
||||
## Phase 24: Onboarding + First-Run [PENDIENTE]
|
||||
|
||||
- [ ] 24.1: Detectar first-run en frontend (GET /api/auth/setup-required)
|
||||
- [ ] 24.2: Wizard multi-step: paso 1 crear admin, paso 2 nombre org, paso 3 "Start your first exploration" con URL input
|
||||
- [ ] 24.3: Empty states: ilustraciones/mensajes en tablas vacías ("No findings yet. Start an exploration!")
|
||||
- [ ] 24.4: Commit: `fase(24): onboarding and first-run experience`
|
||||
|
||||
---
|
||||
|
||||
## Phase 25: Polish + Quality [PENDIENTE]
|
||||
|
||||
- [ ] 25.1: Audit TypeScript strict — eliminar TODOS los `any` restantes
|
||||
- [ ] 25.2: Loading skeletons en todas las pages (shadcn Skeleton)
|
||||
- [ ] 25.3: Error boundaries en cada page
|
||||
- [ ] 25.4: Keyboard shortcuts: ⌘K (command palette), Esc (close dialogs), N (new exploration from dashboard)
|
||||
- [ ] 25.5: Responsive mobile: sidebar collapse, tables scroll, forms stack
|
||||
- [ ] 25.6: README.md profesional: badges (build, license, version), screenshots, features list, quick start, CLI docs, architecture diagram, contributing
|
||||
- [ ] 25.7: CONTRIBUTING.md
|
||||
- [ ] 25.8: LICENSE files: MIT para core, archivo LICENSE-ENTERPRISE separado
|
||||
- [ ] 25.9: Commit: `fase(25): polish and quality improvements`
|
||||
|
||||
---
|
||||
|
||||
## Phase 26: SSO Enterprise [PENDIENTE — ENTERPRISE ONLY]
|
||||
|
||||
- [ ] 26.1: SAML 2.0 via @node-saml/passport-saml con MultiSamlStrategy
|
||||
- [ ] 26.2: OIDC via openid-client (Okta, Azure AD, Google Workspace)
|
||||
- [ ] 26.3: Per-organization IdP configuration
|
||||
- [ ] 26.4: LDAP/AD integration via passport-ldapauth
|
||||
- [ ] 26.5: MFA (TOTP) support
|
||||
- [ ] 26.6: Audit log completo (who did what, when)
|
||||
- [ ] 26.7: Session management dashboard (ver/revocar sessions activas)
|
||||
- [ ] 26.8: Feature-gated tras LICENSE enterprise
|
||||
- [ ] 26.9: Commit: `fase(26): enterprise sso saml oidc ldap`
|
||||
|
||||
---
|
||||
|
||||
## Phase 27: Advanced Enterprise [PENDIENTE — ENTERPRISE ONLY]
|
||||
|
||||
- [ ] 27.1: Data retention policies (auto-delete findings > X days)
|
||||
- [ ] 27.2: Backup/restore CLI tool
|
||||
- [ ] 27.3: White-labeling (CSS custom properties + logo upload)
|
||||
- [ ] 27.4: PostgreSQL support validado end-to-end
|
||||
- [ ] 27.5: Email notifications (nodemailer + templates)
|
||||
- [ ] 27.6: Kubernetes Helm chart
|
||||
- [ ] 27.7: Commit: `fase(27): advanced enterprise features`
|
||||
|
||||
1
.ralph/progress.json
Normal file
1
.ralph/progress.json
Normal file
@@ -0,0 +1 @@
|
||||
{"status": "completed", "timestamp": "2026-03-04 04:32:44"}
|
||||
130
.ralph/specs/legacy/ai-enrichment.md
Normal file
130
.ralph/specs/legacy/ai-enrichment.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# ABE — AI Bug Report Enrichment Specification
|
||||
|
||||
## Concepto
|
||||
Este es el diferenciador más importante de ABE frente a cualquier competidor.
|
||||
Después de detectar una anomalía, ABE puede usar una LLM para enriquecer
|
||||
el bug report con un análisis inteligente: causa probable, impacto,
|
||||
sugerencia de fix, y prompt listo para usar con Claude/GPT.
|
||||
|
||||
## IMPORTANTE: esto es una capa OPCIONAL sobre el core determinista.
|
||||
El core engine nunca llama a LLMs. El enriquecimiento es post-procesado,
|
||||
ejecutado solo si el usuario lo configura.
|
||||
|
||||
## Qué genera la IA
|
||||
|
||||
### 1. Root Cause Analysis
|
||||
A partir del action trace, HTTP log, console errors y DOM snapshot,
|
||||
la IA propone la causa más probable del bug.
|
||||
Ejemplo: "The 500 error is likely caused by missing server-side validation
|
||||
of the email field. The server crashes when receiving an empty string
|
||||
where a valid email is expected."
|
||||
|
||||
### 2. User Impact Assessment
|
||||
La IA evalúa el impacto del bug en términos de negocio:
|
||||
"This bug blocks users from completing registration. Any user who
|
||||
submits an empty email will encounter an unhandled server error,
|
||||
preventing account creation."
|
||||
|
||||
### 3. Suggested Fix
|
||||
La IA propone un fix concreto:
|
||||
"Add server-side validation: check if email is present and valid
|
||||
before processing. Return a 422 with a descriptive error message
|
||||
instead of propagating the exception."
|
||||
|
||||
### 4. AI-Ready Debug Prompt
|
||||
Un prompt completo listo para copiar y pegar en Claude/ChatGPT:
|
||||
```
|
||||
Bug Report Context:
|
||||
- Type: HTTP 500 on form submission
|
||||
- Steps to reproduce: [exact action trace]
|
||||
- Error: [exact error message]
|
||||
- Request: POST /api/register with body {"email": ""}
|
||||
- Response: 500 Internal Server Error
|
||||
|
||||
Please analyze this bug and provide:
|
||||
1. Root cause
|
||||
2. Code fix
|
||||
3. Test case to prevent regression
|
||||
```
|
||||
|
||||
## Implementación
|
||||
|
||||
### Provider abstraction
|
||||
```typescript
|
||||
interface IAIProvider {
|
||||
name: string;
|
||||
enrich(anomaly: IAnomaly, context: IEnrichmentContext): Promise<IAIEnrichment>;
|
||||
}
|
||||
|
||||
interface IEnrichmentContext {
|
||||
domSnapshot: string;
|
||||
httpLog: IHttpResponse[];
|
||||
consoleErrors: string[];
|
||||
actionTrace: IAction[];
|
||||
pageTitle: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface IAIEnrichment {
|
||||
rootCause: string;
|
||||
userImpact: string;
|
||||
suggestedFix: string;
|
||||
debugPrompt: string;
|
||||
confidence: 'low' | 'medium' | 'high';
|
||||
generatedAt: number;
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Providers implementados
|
||||
- `ClaudeProvider` — usa Anthropic API (claude-3-5-haiku — rápido y barato)
|
||||
- `OpenAIProvider` — usa OpenAI API (gpt-4o-mini)
|
||||
- `OllamaProvider` — usa Ollama local (llama3.2 — sin API key, offline)
|
||||
|
||||
### Cuándo se ejecuta
|
||||
- Automático: si `aiEnrichment.autoEnrich: true`, se ejecuta tras cada anomalía high/critical
|
||||
- Manual: botón "Enrich with AI" en AnomalyDetail page
|
||||
- No bloquea: el bug report se guarda sin enriquecimiento, la IA lo añade async
|
||||
|
||||
## Configuración en .env
|
||||
```
|
||||
ABE_AI_PROVIDER=claude # claude | openai | ollama | none
|
||||
ABE_AI_API_KEY=sk-ant-xxx # Anthropic key (si provider=claude)
|
||||
ABE_OPENAI_API_KEY=sk-xxx # OpenAI key (si provider=openai)
|
||||
ABE_OLLAMA_URL=http://localhost:11434 # (si provider=ollama)
|
||||
ABE_AI_MODEL=claude-haiku-4-5 # modelo específico (opcional)
|
||||
ABE_AI_AUTO_ENRICH=false # default false para no incurrir en costes
|
||||
ABE_AI_MIN_SEVERITY=high # solo enriquecer high/critical automáticamente
|
||||
```
|
||||
|
||||
## Modelo de datos — añadir a SQLite
|
||||
|
||||
### Añadir columna a anomalies
|
||||
```sql
|
||||
ALTER TABLE anomalies ADD COLUMN ai_enrichment_json TEXT;
|
||||
ALTER TABLE anomalies ADD COLUMN ai_enriched_at INTEGER;
|
||||
```
|
||||
|
||||
## Frontend — AI panel en AnomalyDetail
|
||||
|
||||
Si la anomalía tiene ai_enrichment_json, mostrar panel "AI Analysis" con:
|
||||
- 🔍 Root Cause (texto con ícono)
|
||||
- 👥 User Impact (texto con ícono)
|
||||
- 🔧 Suggested Fix (bloque de código si contiene código)
|
||||
- 📋 "Copy debug prompt" button (copia el debugPrompt al clipboard)
|
||||
- Badge: "Analyzed by Claude" / "Analyzed by GPT-4o-mini" / "Analyzed by Llama 3.2"
|
||||
- Timestamp de cuándo se generó
|
||||
|
||||
Si no tiene enriquecimiento, mostrar botón "✨ Analyze with AI" que llama a:
|
||||
POST /api/anomalies/:id/enrich
|
||||
|
||||
## Endpoint nuevo
|
||||
|
||||
### POST /api/anomalies/:anomalyId/enrich
|
||||
Dispara el enriquecimiento de una anomalía concreta (async).
|
||||
Response inmediata: { status: 'enriching' }
|
||||
Cuando termina, emite WebSocket event: anomaly:enriched { anomalyId, enrichment }
|
||||
|
||||
### GET /api/anomalies/:anomalyId — actualizado
|
||||
Incluye ai_enrichment si está disponible.
|
||||
59
.ralph/specs/legacy/api-security.md
Normal file
59
.ralph/specs/legacy/api-security.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# ABE — API Security Specification
|
||||
|
||||
## Authentication: API Key
|
||||
|
||||
All API endpoints require an API key passed in the header:
|
||||
`X-ABE-API-Key: <key>`
|
||||
|
||||
If missing or invalid → 401 Unauthorized.
|
||||
|
||||
## Configuration
|
||||
|
||||
API key is set via environment variable: `ABE_API_KEY`
|
||||
If not set, server logs a warning and runs without auth (dev mode only).
|
||||
|
||||
## Implementation
|
||||
|
||||
Create `src/server/middleware/auth.ts`:
|
||||
```typescript
|
||||
export function apiKeyAuth(req, res, next) {
|
||||
const apiKey = process.env.ABE_API_KEY;
|
||||
if (!apiKey) return next(); // dev mode: no auth
|
||||
const provided = req.headers['x-abe-api-key'];
|
||||
if (!provided || provided !== apiKey) {
|
||||
return res.status(401).json({ error: 'Invalid or missing API key' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
Apply this middleware to ALL routes EXCEPT:
|
||||
- GET /health
|
||||
- GET /ready
|
||||
|
||||
## CORS
|
||||
|
||||
Only allow requests from the frontend origin.
|
||||
Configure via environment variable: `ABE_CORS_ORIGIN` (default: `http://localhost:5173`)
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Add `express-rate-limit`:
|
||||
- Max 20 POST /api/sessions per hour per IP
|
||||
- Max 200 requests per minute per IP for other endpoints
|
||||
|
||||
## Environment Variables (full list for .env)
|
||||
```
|
||||
ABE_API_KEY=change-me-in-production
|
||||
ABE_CORS_ORIGIN=http://localhost:5173
|
||||
ABE_PORT=3001
|
||||
ABE_DB_PATH=./data/abe.db
|
||||
ABE_REPORTS_DIR=./reports
|
||||
ABE_LOGS_DIR=./logs
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
## docker-compose update
|
||||
|
||||
Add .env file support and environment variables to docker-compose.yml.
|
||||
Add a volumes entry for `data/` directory for SQLite persistence.
|
||||
187
.ralph/specs/legacy/api-server.md
Normal file
187
.ralph/specs/legacy/api-server.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# ABE — API Server Specification
|
||||
|
||||
## Arquitectura general
|
||||
```
|
||||
React (puerto 5173)
|
||||
↕ HTTP REST + WebSocket
|
||||
API Server Express (puerto 3001)
|
||||
↕ imports directos
|
||||
ExplorationEngine (core)
|
||||
```
|
||||
|
||||
El servidor vive en `src/server/` y es el único punto de entrada al motor desde el exterior. El frontend NUNCA importa código del core directamente.
|
||||
|
||||
---
|
||||
|
||||
## Tecnología del servidor
|
||||
|
||||
- Framework: Express.js
|
||||
- WebSocket: socket.io (para streaming en tiempo real)
|
||||
- Archivos: `src/server/index.ts` y `src/server/routes/`
|
||||
|
||||
---
|
||||
|
||||
## REST Endpoints
|
||||
|
||||
### POST /api/sessions
|
||||
Lanza una nueva exploración.
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:3000",
|
||||
"seed": 42,
|
||||
"maxStates": 50
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"status": "running",
|
||||
"startedAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/sessions
|
||||
Lista todas las sesiones (activas e históricas).
|
||||
|
||||
Response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"url": "http://localhost:3000",
|
||||
"status": "running",
|
||||
"startedAt": "2025-01-15T10:00:00.000Z",
|
||||
"anomaliesFound": 3,
|
||||
"statesVisited": 12
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/sessions/:sessionId
|
||||
Detalle de una sesión específica.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"url": "http://localhost:3000",
|
||||
"status": "completed",
|
||||
"startedAt": "2025-01-15T10:00:00.000Z",
|
||||
"finishedAt": "2025-01-15T10:05:00.000Z",
|
||||
"statesVisited": 12,
|
||||
"anomaliesFound": 3,
|
||||
"seed": 42
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/sessions/:sessionId
|
||||
Detiene una sesión activa.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "stopped": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/anomalies
|
||||
Lista todas las anomalías encontradas (todas las sesiones).
|
||||
|
||||
Query params opcionales: `?sessionId=sess_abc123&severity=high`
|
||||
|
||||
Response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "anom_a1b2c3",
|
||||
"sessionId": "sess_abc123",
|
||||
"type": "http_error",
|
||||
"severity": "high",
|
||||
"description": "Form returns HTTP 500 on empty email",
|
||||
"timestamp": 1705312200000,
|
||||
"screenshotUrl": "/api/anomalies/anom_a1b2c3/screenshot"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /api/anomalies/:anomalyId
|
||||
Detalle completo de una anomalía incluyendo pasos de reproducción.
|
||||
|
||||
Response: el objeto IAnomaly completo serializado (definido en interfaces.md)
|
||||
|
||||
---
|
||||
|
||||
### GET /api/anomalies/:anomalyId/screenshot
|
||||
Devuelve la imagen PNG del screenshot de la anomalía.
|
||||
|
||||
Response: imagen binaria con Content-Type: image/png
|
||||
|
||||
---
|
||||
|
||||
### POST /api/anomalies/:anomalyId/replay
|
||||
Lanza el replay de una anomalía específica.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"replayId": "replay_xyz",
|
||||
"status": "running"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Events (socket.io)
|
||||
|
||||
El cliente se conecta a `ws://localhost:3001` y escucha estos eventos:
|
||||
|
||||
### Eventos que emite el SERVIDOR → cliente
|
||||
|
||||
`session:started`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "url": "http://localhost:3000" }
|
||||
```
|
||||
|
||||
`state:discovered`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "stateId": "s_xyz", "url": "/register", "title": "Register" }
|
||||
```
|
||||
|
||||
`action:executed`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "actionType": "click", "selector": "button#submit", "timestamp": 1705312197000 }
|
||||
```
|
||||
|
||||
`anomaly:detected`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "anomalyId": "anom_a1b2c3", "type": "http_error", "severity": "high", "description": "..." }
|
||||
```
|
||||
|
||||
`session:completed`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "statesVisited": 12, "anomaliesFound": 3 }
|
||||
```
|
||||
|
||||
`session:error`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123", "error": "Target URL unreachable" }
|
||||
```
|
||||
|
||||
### Eventos que emite el CLIENTE → servidor
|
||||
|
||||
`session:stop`
|
||||
```json
|
||||
{ "sessionId": "sess_abc123" }
|
||||
```
|
||||
118
.ralph/specs/legacy/cli-cicd.md
Normal file
118
.ralph/specs/legacy/cli-cicd.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# ABE — CLI & CI/CD Integration Specification
|
||||
|
||||
## CLI Entry Point
|
||||
|
||||
File: `src/cli.ts`
|
||||
Script in package.json: `"abe": "ts-node src/cli.ts"`
|
||||
Global after install: `npx abe` or `abe` if installed globally.
|
||||
|
||||
## CLI Usage
|
||||
```bash
|
||||
# Basic run
|
||||
abe run --url http://localhost:3000
|
||||
|
||||
# With auth
|
||||
abe run --url http://app.com \
|
||||
--auth-type login_flow \
|
||||
--login-url http://app.com/login \
|
||||
--username test@app.com \
|
||||
--password secret
|
||||
|
||||
# With scope limits
|
||||
abe run --url http://app.com \
|
||||
--max-states 30 \
|
||||
--max-depth 4 \
|
||||
--allowed-domains app.com
|
||||
|
||||
# CI mode: exit 1 if any anomaly found
|
||||
abe run --url http://localhost:3000 --fail-on-anomaly
|
||||
|
||||
# CI mode: exit 1 only on high/critical anomalies
|
||||
abe run --url http://localhost:3000 --fail-on-severity high
|
||||
|
||||
# Output formats
|
||||
abe run --url http://localhost:3000 --output json # prints JSON summary to stdout
|
||||
abe run --url http://localhost:3000 --output junit # generates junit.xml for CI
|
||||
|
||||
# Connect to a running ABE server instead of running inline
|
||||
abe run --url http://localhost:3000 --server http://abe-server:3001 --api-key mykey
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- 0 → exploration complete, no anomalies (or no anomalies above threshold)
|
||||
- 1 → anomalies found above threshold
|
||||
- 2 → exploration failed (target unreachable, auth failed, etc.)
|
||||
|
||||
## stdout JSON output (--output json)
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_abc123",
|
||||
"url": "http://localhost:3000",
|
||||
"duration_ms": 45000,
|
||||
"states_visited": 12,
|
||||
"anomalies": [
|
||||
{
|
||||
"id": "anom_xyz",
|
||||
"type": "http_error",
|
||||
"severity": "high",
|
||||
"description": "Form returns 500 on empty email",
|
||||
"report_path": "reports/anom_xyz/report.json"
|
||||
}
|
||||
],
|
||||
"exit_code": 1
|
||||
}
|
||||
```
|
||||
|
||||
## JUnit XML output (--output junit)
|
||||
|
||||
Generates `abe-results.xml` compatible with Jenkins, GitHub Actions, GitLab CI:
|
||||
- Each anomaly = one failing test case
|
||||
- Each explored state = one passing test case
|
||||
|
||||
## GitHub Actions Example Workflow
|
||||
|
||||
Create file: `.github/workflows/abe-example.yml` in the repo:
|
||||
```yaml
|
||||
name: ABE Exploratory Testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
explore:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Start application
|
||||
run: docker-compose up -d app
|
||||
# assumes the project has a docker-compose with the target app
|
||||
|
||||
- name: Wait for app
|
||||
run: npx wait-on http://localhost:3000 --timeout 30000
|
||||
|
||||
- name: Run ABE
|
||||
run: |
|
||||
npm install -g abe-explorer # or: npx abe
|
||||
abe run \
|
||||
--url http://localhost:3000 \
|
||||
--max-states 30 \
|
||||
--fail-on-severity high \
|
||||
--output junit
|
||||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: abe-reports
|
||||
path: reports/
|
||||
|
||||
- name: Publish test results
|
||||
if: always()
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
with:
|
||||
files: abe-results.xml
|
||||
```
|
||||
99
.ralph/specs/legacy/database.md
Normal file
99
.ralph/specs/legacy/database.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# ABE — Database Specification (SQLite)
|
||||
|
||||
## Rationale
|
||||
File-based storage loses all data on container restart.
|
||||
SQLite requires zero extra services and is perfect for self-hosted deployment.
|
||||
|
||||
## Library
|
||||
Use `better-sqlite3` (synchronous, faster than async alternatives for this use case).
|
||||
|
||||
## Location
|
||||
Database file: `data/abe.db` (persisted via Docker volume)
|
||||
|
||||
## Schema
|
||||
|
||||
### Table: sessions
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
seed INTEGER NOT NULL,
|
||||
max_states INTEGER NOT NULL DEFAULT 50,
|
||||
states_visited INTEGER NOT NULL DEFAULT 0,
|
||||
anomalies_found INTEGER NOT NULL DEFAULT 0,
|
||||
started_at INTEGER NOT NULL,
|
||||
finished_at INTEGER,
|
||||
config_json TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
```
|
||||
|
||||
### Table: states
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS states (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
url TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
dom_snapshot_path TEXT,
|
||||
visit_count INTEGER NOT NULL DEFAULT 0,
|
||||
discovered_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Table: actions
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS actions (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
state_id TEXT NOT NULL REFERENCES states(id),
|
||||
type TEXT NOT NULL,
|
||||
selector TEXT,
|
||||
value TEXT,
|
||||
url TEXT,
|
||||
seed INTEGER NOT NULL,
|
||||
executed_at INTEGER NOT NULL,
|
||||
sequence_order INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Table: anomalies
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS anomalies (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
type TEXT NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
action_trace_json TEXT NOT NULL,
|
||||
evidence_json TEXT NOT NULL,
|
||||
screenshot_path TEXT,
|
||||
dom_snapshot_path TEXT,
|
||||
detected_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Table: notifications
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
anomaly_id TEXT NOT NULL REFERENCES anomalies(id),
|
||||
channel TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
sent_at INTEGER,
|
||||
error TEXT
|
||||
);
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
Create `src/db/` with:
|
||||
- `src/db/connection.ts` — singleton SQLite connection, runs migrations on startup
|
||||
- `src/db/SessionRepository.ts` — CRUD for sessions
|
||||
- `src/db/AnomalyRepository.ts` — CRUD for anomalies, includes filter by session/severity
|
||||
- `src/db/migrations.ts` — runs all CREATE TABLE IF NOT EXISTS on startup
|
||||
|
||||
## Rules
|
||||
- All DB operations are synchronous (better-sqlite3 is sync)
|
||||
- Repositories are injected into the API server, never imported directly by core engine
|
||||
- The engine emits events → the API server listens and persists to DB
|
||||
102
.ralph/specs/legacy/docker.md
Normal file
102
.ralph/specs/legacy/docker.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# ABE — Docker Specification
|
||||
|
||||
## Objetivo
|
||||
Permitir arrancar todo el proyecto (backend + frontend) con un solo comando:
|
||||
docker-compose up --build
|
||||
|
||||
## Backend Dockerfile (raíz del proyecto)
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/server/index.js"]
|
||||
```
|
||||
|
||||
## Frontend Dockerfile (frontend/Dockerfile)
|
||||
|
||||
Usa build multistage: primero compila con Node, luego sirve con nginx.
|
||||
```dockerfile
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
## nginx.conf (frontend/nginx.conf)
|
||||
|
||||
Necesario para que React Router funcione correctamente (todas las rutas apuntan a index.html):
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:3001;
|
||||
}
|
||||
|
||||
location /socket.io {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## docker-compose.yml (raíz)
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3001
|
||||
volumes:
|
||||
- ./reports:/app/reports
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- abe-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5173:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- abe-network
|
||||
|
||||
networks:
|
||||
abe-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
## Notas importantes
|
||||
- El frontend en producción (nginx) hace proxy de /api y /socket.io al backend
|
||||
- Los volúmenes reports/ y logs/ persisten datos entre reinicios del contenedor
|
||||
- El frontend se accede en http://localhost:5173
|
||||
- El backend se accede en http://localhost:3001
|
||||
84
.ralph/specs/legacy/exploration-config.md
Normal file
84
.ralph/specs/legacy/exploration-config.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# ABE — Exploration Scope & Target Authentication Specification
|
||||
|
||||
## Exploration Config Object
|
||||
|
||||
This config is passed via POST /api/sessions and stored in sessions.config_json.
|
||||
```typescript
|
||||
interface ExplorationConfig {
|
||||
// Scope
|
||||
allowedDomains: string[]; // e.g. ["localhost", "myapp.com"] — never follow external links
|
||||
maxStates: number; // default: 50 — stop after this many unique states
|
||||
maxDepth: number; // default: 5 — max click depth from start URL
|
||||
actionDelayMs: number; // default: 500 — wait between actions (politeness)
|
||||
sessionTimeoutMs: number; // default: 300000 (5 min) — hard stop
|
||||
|
||||
// Exclusions
|
||||
excludedPaths: string[]; // e.g. ["/logout", "/admin"] — never navigate here
|
||||
excludedSelectors: string[]; // e.g. ["button.delete", "a[href*='delete']"]
|
||||
|
||||
// Target authentication
|
||||
auth: AuthConfig | null;
|
||||
|
||||
// Fuzzing
|
||||
fuzzingEnabled: boolean; // default: true
|
||||
fuzzingIntensity: 'low' | 'medium' | 'high'; // default: 'medium'
|
||||
}
|
||||
|
||||
type AuthConfig =
|
||||
| { type: 'cookies'; cookies: Array<{ name: string; value: string; domain: string }> }
|
||||
| { type: 'headers'; headers: Record<string, string> }
|
||||
| { type: 'login_flow'; loginUrl: string; usernameSelector: string; passwordSelector: string; submitSelector: string; username: string; password: string }
|
||||
```
|
||||
|
||||
## Scope Rules (enforced in PlaywrightAgent)
|
||||
|
||||
1. Before navigating to any URL, check if hostname is in allowedDomains. If not, skip.
|
||||
2. Before executing any action, check if current path matches excludedPaths. If yes, skip.
|
||||
3. Before clicking any element, check if it matches excludedSelectors. If yes, skip.
|
||||
4. Stop exploration when statesVisited >= maxStates OR depth >= maxDepth OR elapsed > sessionTimeoutMs.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### type: 'cookies'
|
||||
Inject cookies before the first navigation using playwright context.addCookies().
|
||||
|
||||
### type: 'headers'
|
||||
Set extra HTTP headers on the browser context using context.setExtraHTTPHeaders().
|
||||
|
||||
### type: 'login_flow'
|
||||
Before starting exploration:
|
||||
1. Navigate to loginUrl
|
||||
2. Fill usernameSelector with username
|
||||
3. Fill passwordSelector with password
|
||||
4. Click submitSelector
|
||||
5. Wait for navigation to complete
|
||||
6. Verify we are no longer on loginUrl (if still there, login failed → abort session with error)
|
||||
7. Proceed with exploration from startUrl
|
||||
|
||||
## Updated POST /api/sessions request body
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:3000",
|
||||
"seed": 42,
|
||||
"config": {
|
||||
"allowedDomains": ["localhost"],
|
||||
"maxStates": 50,
|
||||
"maxDepth": 5,
|
||||
"actionDelayMs": 500,
|
||||
"sessionTimeoutMs": 300000,
|
||||
"excludedPaths": ["/logout"],
|
||||
"excludedSelectors": [],
|
||||
"auth": {
|
||||
"type": "login_flow",
|
||||
"loginUrl": "http://localhost:3000/login",
|
||||
"usernameSelector": "input[name='email']",
|
||||
"passwordSelector": "input[name='password']",
|
||||
"submitSelector": "button[type='submit']",
|
||||
"username": "test@example.com",
|
||||
"password": "password123"
|
||||
},
|
||||
"fuzzingEnabled": true,
|
||||
"fuzzingIntensity": "medium"
|
||||
}
|
||||
}
|
||||
```
|
||||
72
.ralph/specs/legacy/frontend-v2.md
Normal file
72
.ralph/specs/legacy/frontend-v2.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# ABE — Frontend v2 Specification
|
||||
|
||||
## New pages and components to add
|
||||
|
||||
### New Page: Settings (ruta: /settings)
|
||||
|
||||
Sections:
|
||||
1. API Key — show current key, button to copy
|
||||
2. Notifications — form to set Slack webhook URL and min severity (calls PATCH /api/config)
|
||||
3. Default Exploration Config — form with default values for maxStates, maxDepth, delay, excluded paths
|
||||
4. About — version, links to docs
|
||||
|
||||
### Updated: NewSessionForm
|
||||
|
||||
Add fields:
|
||||
- Allowed Domains (chips input, default: hostname of URL)
|
||||
- Max States (number, default 50)
|
||||
- Max Depth (number, default 5)
|
||||
- Action Delay ms (number, default 500)
|
||||
- Excluded Paths (chips input)
|
||||
- Auth Type (select: none / cookies / headers / login_flow)
|
||||
- If login_flow: show loginUrl, usernameSelector, passwordSelector, submitSelector, username, password
|
||||
- If cookies: textarea for JSON cookie array
|
||||
- If headers: key-value pairs input
|
||||
- Fuzzing enabled (toggle)
|
||||
- Fuzzing intensity (select: low / medium / high)
|
||||
|
||||
### Updated: Dashboard
|
||||
|
||||
Add stats bar at the top with 4 numbers:
|
||||
- Total sessions
|
||||
- Total anomalies found
|
||||
- Critical/High anomalies (highlighted in red)
|
||||
- Sessions running now
|
||||
|
||||
### Updated: AnomalyList
|
||||
|
||||
Add filter bar:
|
||||
- Filter by severity (multi-select: low, medium, high, critical)
|
||||
- Filter by type (multi-select: http_error, js_exception, etc.)
|
||||
- Filter by session (dropdown)
|
||||
- Search by description (text input)
|
||||
- Sort by: newest first / severity desc
|
||||
|
||||
### Updated: AnomalyDetail
|
||||
|
||||
Add:
|
||||
- Download button → downloads report.json
|
||||
- Download MD button → downloads report.md
|
||||
- Copy replay command button → copies `abe replay --anomaly-id anom_xxx` to clipboard
|
||||
|
||||
### New Component: SeverityBadge
|
||||
|
||||
Reusable badge component used everywhere:
|
||||
- critical → red bg, white text
|
||||
- high → orange bg, white text
|
||||
- medium → yellow bg, dark text
|
||||
- low → blue bg, white text
|
||||
|
||||
### New API endpoints needed (add to api-server spec)
|
||||
|
||||
PATCH /api/config
|
||||
- Updates server config (slack webhook, min severity, defaults)
|
||||
- Body: Partial<ServerConfig>
|
||||
- Returns: updated ServerConfig
|
||||
|
||||
GET /api/config
|
||||
- Returns current server config (without API key value)
|
||||
|
||||
GET /api/stats
|
||||
- Returns: { totalSessions, totalAnomalies, criticalHighCount, runningSessions }
|
||||
- Used by dashboard stats bar
|
||||
99
.ralph/specs/legacy/frontend.md
Normal file
99
.ralph/specs/legacy/frontend.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# ABE — Frontend Specification
|
||||
|
||||
## Tecnología
|
||||
- React 18 + TypeScript
|
||||
- Vite (bundler, más simple que webpack)
|
||||
- TailwindCSS (estilos sin escribir CSS manual)
|
||||
- socket.io-client (WebSocket)
|
||||
- React Router v6 (navegación entre páginas)
|
||||
|
||||
## Ubicación
|
||||
El frontend vive en `frontend/` en la raíz del proyecto, completamente separado de `src/`.
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ ├── Dashboard.tsx ← página principal
|
||||
│ │ ├── SessionDetail.tsx ← detalle de una sesión en vivo
|
||||
│ │ └── AnomalyDetail.tsx ← detalle de un bug report
|
||||
│ ├── components/
|
||||
│ │ ├── NewSessionForm.tsx ← formulario para lanzar exploración
|
||||
│ │ ├── SessionList.tsx ← lista de sesiones
|
||||
│ │ ├── AnomalyList.tsx ← lista de anomalías
|
||||
│ │ ├── LiveFeed.tsx ← stream en tiempo real de eventos
|
||||
│ │ └── AnomalyCard.tsx ← tarjeta de una anomalía
|
||||
│ ├── hooks/
|
||||
│ │ ├── useSocket.ts ← conexión WebSocket reutilizable
|
||||
│ │ └── useApi.ts ← fetch helper para la API REST
|
||||
│ ├── types.ts ← tipos TypeScript del frontend (espejo de interfaces.ts)
|
||||
│ ├── App.tsx ← router principal
|
||||
│ └── main.tsx ← entry point
|
||||
├── index.html
|
||||
├── vite.config.ts
|
||||
├── tailwind.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Página 1 — Dashboard (ruta: `/`)
|
||||
|
||||
Contiene:
|
||||
- Botón "New Exploration" que abre el formulario
|
||||
- `NewSessionForm`: campos URL y Seed, botón Start
|
||||
- `SessionList`: tabla con todas las sesiones (estado, URL, anomalías encontradas, fecha)
|
||||
- `AnomalyList`: lista de las últimas anomalías de todas las sesiones
|
||||
|
||||
---
|
||||
|
||||
## Página 2 — Session Detail (ruta: `/sessions/:sessionId`)
|
||||
|
||||
Contiene:
|
||||
- Header con URL explorada, estado (running/completed), seed
|
||||
- Botón "Stop" si la sesión está activa
|
||||
- `LiveFeed`: lista en tiempo real de eventos WebSocket
|
||||
- Cada evento muestra icono + texto + timestamp
|
||||
- Scroll automático al último evento
|
||||
- Colores: verde para state:discovered, amarillo para action:executed, rojo para anomaly:detected
|
||||
- `AnomalyList`: anomalías encontradas en esta sesión (se actualiza en tiempo real)
|
||||
|
||||
---
|
||||
|
||||
## Página 3 — Anomaly Detail (ruta: `/anomalies/:anomalyId`)
|
||||
|
||||
Contiene:
|
||||
- Header con tipo, severidad (badge de color), descripción
|
||||
- Sección "Reproduction Steps": lista numerada de acciones
|
||||
- Sección "Evidence":
|
||||
- Screenshot a tamaño completo (imagen)
|
||||
- Botón para ver DOM snapshot (abre en nueva pestaña)
|
||||
- Sección "HTTP Log": tabla con requests (URL, método, status, duración)
|
||||
- Sección "Raw Errors": bloque de código con los errores textuales
|
||||
- Botón "Run Replay": llama a POST /api/anomalies/:id/replay y muestra estado
|
||||
|
||||
---
|
||||
|
||||
## Colores de severidad (badges)
|
||||
- critical → rojo (#ef4444)
|
||||
- high → naranja (#f97316)
|
||||
- medium → amarillo (#eab308)
|
||||
- low → azul (#3b82f6)
|
||||
|
||||
---
|
||||
|
||||
## Conexión con la API
|
||||
|
||||
Todas las llamadas van a `http://localhost:3001`.
|
||||
En `vite.config.ts` configurar proxy para `/api` y `/socket.io` apuntando a `localhost:3001`.
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3001',
|
||||
'/socket.io': { target: 'http://localhost:3001', ws: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
94
.ralph/specs/legacy/fuzzing.md
Normal file
94
.ralph/specs/legacy/fuzzing.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# ABE — Fuzzing / Disruption Module Specification
|
||||
|
||||
## Purpose
|
||||
This is ABE's core differentiator. Instead of only clicking valid elements,
|
||||
ABE injects abnormal inputs into forms to provoke unexpected server behavior.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
src/plugins/fuzzers/
|
||||
├── FuzzingEngine.ts ← orchestrator, decides when and how to fuzz
|
||||
├── strategies/
|
||||
│ ├── EmptyValueStrategy.ts
|
||||
│ ├── OversizedStringStrategy.ts
|
||||
│ ├── SpecialCharsStrategy.ts
|
||||
│ ├── TypeMismatchStrategy.ts
|
||||
│ └── BoundaryValueStrategy.ts
|
||||
└── InputTypeDetector.ts ← detects field type from DOM attributes
|
||||
```
|
||||
|
||||
## InputTypeDetector
|
||||
|
||||
Detects field type from: input[type], input[name], input[placeholder], label text, aria-label.
|
||||
```typescript
|
||||
type DetectedInputType =
|
||||
| 'email' | 'password' | 'number' | 'date' | 'phone'
|
||||
| 'url' | 'search' | 'text' | 'textarea' | 'select' | 'file'
|
||||
```
|
||||
|
||||
## Fuzzing Strategies
|
||||
|
||||
### EmptyValueStrategy
|
||||
Submits forms with all fields empty. Catches missing server-side validation.
|
||||
Applies to: all input types.
|
||||
Values: `""`, `" "` (space only), `"\t"` (tab).
|
||||
|
||||
### OversizedStringStrategy
|
||||
Submits strings far beyond expected length. Catches buffer issues and UI overflow.
|
||||
Applies to: text, email, password, textarea.
|
||||
Values by intensity:
|
||||
- low: 256 chars
|
||||
- medium: 1024 chars
|
||||
- high: 10000 chars + unicode chars
|
||||
|
||||
### SpecialCharsStrategy
|
||||
Injects characters that break SQL, HTML, and shell contexts.
|
||||
Applies to: text, email, search, textarea.
|
||||
Values:
|
||||
```
|
||||
' OR 1=1 --
|
||||
<script>alert(1)</script>
|
||||
../../etc/passwd
|
||||
${7*7}
|
||||
\x00\x01\x02
|
||||
```
|
||||
|
||||
### TypeMismatchStrategy
|
||||
Submits wrong data types for the field.
|
||||
- email field → "not-an-email", "12345", "@@@"
|
||||
- number field → "abc", "-999999", "9.9.9", "NaN"
|
||||
- date field → "yesterday", "32/13/2025", "0000-00-00"
|
||||
- url field → "javascript:alert(1)", "not a url"
|
||||
- phone field → "000", "++++", "abcdefghij"
|
||||
|
||||
### BoundaryValueStrategy
|
||||
Tests values at the edges of expected ranges.
|
||||
- number field → 0, -1, 2147483647, 2147483648, -2147483648
|
||||
- date field → "1900-01-01", "2099-12-31", "1970-01-01"
|
||||
|
||||
## Fuzzing Execution Flow
|
||||
```
|
||||
For each form discovered in state:
|
||||
1. InputTypeDetector analyzes each field
|
||||
2. FuzzingEngine selects strategies based on fuzzingIntensity:
|
||||
- low: EmptyValue + TypeMismatch only
|
||||
- medium: + OversizedString + BoundaryValue
|
||||
- high: + SpecialChars
|
||||
3. For each strategy, fill all fields with fuzz values
|
||||
4. Submit the form
|
||||
5. Observe response via AnomalyDetector
|
||||
6. Record results
|
||||
```
|
||||
|
||||
## AnomalyDetector additions for fuzzing
|
||||
|
||||
Add these new anomaly types:
|
||||
- `validation_bypass` — server accepted clearly invalid input (e.g. submitted empty required email, got 200)
|
||||
- `server_error_on_fuzz` — server returned 500 on a fuzzed input
|
||||
- `xss_reflection` — fuzzed script tag appears in response body
|
||||
|
||||
## Integration point
|
||||
|
||||
FuzzingEngine is called from ExplorationEngine AFTER normal action discovery,
|
||||
only when `config.fuzzingEnabled === true`.
|
||||
It is passed as an optional plugin, so the core engine doesn't depend on it directly.
|
||||
164
.ralph/specs/legacy/interfaces.md
Normal file
164
.ralph/specs/legacy/interfaces.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# ABE — Core Interfaces Specification
|
||||
|
||||
## Regla fundamental
|
||||
`src/core/` solo puede importar desde este documento.
|
||||
`src/plugins/` implementa estas interfaces, nunca al revés.
|
||||
|
||||
---
|
||||
|
||||
## IState
|
||||
|
||||
Representa un estado único de la aplicación explorada.
|
||||
```typescript
|
||||
interface IState {
|
||||
id: string; // hash SHA1 del snapshot DOM + URL
|
||||
url: string; // URL completa en este estado
|
||||
title: string; // document.title
|
||||
timestamp: number; // Date.now() cuando se capturó
|
||||
domSnapshot: string; // outerHTML del body serializado
|
||||
visitCount: number; // cuántas veces se ha visitado este estado
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IAction
|
||||
|
||||
Representa una acción que el agente puede ejecutar.
|
||||
```typescript
|
||||
interface IAction {
|
||||
id: string; // uuid v4 generado al crear la acción
|
||||
type: 'click' | 'fill' | 'navigate' | 'select' | 'submit';
|
||||
selector?: string; // CSS selector del elemento (si aplica)
|
||||
value?: string; // valor a introducir (para fill/select)
|
||||
url?: string; // destino (solo para navigate)
|
||||
timestamp: number; // cuando se ejecutó
|
||||
seed: number; // semilla usada para selección aleatoria
|
||||
stateId: string; // ID del estado desde el que se ejecutó
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IObservation
|
||||
|
||||
Lo que el agente observa DESPUÉS de ejecutar una acción.
|
||||
```typescript
|
||||
interface IObservation {
|
||||
id: string; // uuid v4
|
||||
actionId: string; // acción que provocó esta observación
|
||||
newStateId: string; // ID del nuevo estado resultante
|
||||
httpResponses: IHttpResponse[]; // todas las requests durante la acción
|
||||
consoleErrors: string[]; // mensajes de console.error capturados
|
||||
jsExceptions: string[]; // excepciones JS no capturadas
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface IHttpResponse {
|
||||
url: string;
|
||||
status: number;
|
||||
method: string;
|
||||
durationMs: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IAnomaly
|
||||
|
||||
Una desviación detectada del comportamiento esperado.
|
||||
```typescript
|
||||
interface IAnomaly {
|
||||
id: string; // uuid v4
|
||||
type: AnomalyType;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
observationId: string; // observación que la provocó
|
||||
actionTrace: IAction[]; // secuencia exacta de acciones que llevaron aquí
|
||||
description: string; // texto legible explicando qué pasó
|
||||
evidence: IAnomalyEvidence;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type AnomalyType =
|
||||
| 'http_error' // respuesta HTTP 4xx o 5xx
|
||||
| 'js_exception' // excepción JavaScript no capturada
|
||||
| 'console_error' // console.error detectado
|
||||
| 'navigation_fail' // navegación no completada
|
||||
| 'element_missing' // elemento esperado desaparece
|
||||
| 'timeout'; // acción excede tiempo límite
|
||||
|
||||
interface IAnomalyEvidence {
|
||||
screenshotPath?: string; // ruta relativa al screenshot
|
||||
domSnapshotPath?: string; // ruta relativa al DOM serializado
|
||||
httpLog?: IHttpResponse[]; // requests relevantes
|
||||
rawErrors?: string[]; // errores textuales originales
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IInteractionAgent (plugin interface)
|
||||
|
||||
Lo que cualquier agente de interacción debe implementar.
|
||||
```typescript
|
||||
interface IInteractionAgent {
|
||||
launch(url: string): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
discoverActions(state: IState): Promise<IAction[]>;
|
||||
executeAction(action: IAction): Promise<IObservation>;
|
||||
captureState(): Promise<IState>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ICollector (plugin interface)
|
||||
|
||||
Lo que cualquier colector de contexto debe implementar.
|
||||
```typescript
|
||||
interface ICollector {
|
||||
name: string;
|
||||
collect(anomaly: IAnomaly, agent: IInteractionAgent): Promise<IAnomalyEvidence>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IReproducer
|
||||
|
||||
Genera un script de replay a partir de una traza de acciones.
|
||||
```typescript
|
||||
interface IReproducer {
|
||||
serialize(trace: IAction[]): string; // JSON serializado
|
||||
deserialize(raw: string): IAction[]; // reconstruye la traza
|
||||
generateScript(trace: IAction[]): string; // script Playwright ejecutable
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IExporter (plugin interface)
|
||||
|
||||
Transforma una anomalía en un reporte consumible.
|
||||
```typescript
|
||||
interface IExporter {
|
||||
format: 'markdown' | 'json';
|
||||
export(anomaly: IAnomaly, outputDir: string): Promise<string>; // retorna la ruta del archivo generado
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## StateGraph
|
||||
|
||||
No es una interfaz pero su contrato debe ser explícito.
|
||||
```typescript
|
||||
class StateGraph {
|
||||
addState(state: IState): void;
|
||||
hasState(stateId: string): boolean;
|
||||
recordTransition(fromId: string, action: IAction, toId: string): void;
|
||||
getUnvisited(): IState[]; // estados con visitCount === 0
|
||||
getNextToExplore(): IState | null; // heurística BFS por defecto
|
||||
toJSON(): object; // serializable para logs
|
||||
}
|
||||
```
|
||||
119
.ralph/specs/legacy/multi-browser-accessibility.md
Normal file
119
.ralph/specs/legacy/multi-browser-accessibility.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# ABE — Multi-Browser, Mobile Emulation & Accessibility Specification
|
||||
|
||||
## Multi-browser testing
|
||||
|
||||
### Browsers soportados (via Playwright)
|
||||
- chromium (Chrome/Edge) — siempre disponible
|
||||
- firefox — opcional
|
||||
- webkit (Safari) — opcional
|
||||
|
||||
### Configuración en ExplorationConfig
|
||||
```typescript
|
||||
browsers: Array<'chromium' | 'firefox' | 'webkit'>; // default: ['chromium']
|
||||
```
|
||||
|
||||
### Comportamiento
|
||||
Cuando se especifican múltiples browsers:
|
||||
- ABE ejecuta la misma exploración en paralelo en cada browser
|
||||
- Cada browser crea su propia sub-sesión con el mismo seed
|
||||
- Los resultados se agrupan bajo la misma sesión padre
|
||||
- Las anomalías incluyen qué browser las detectó
|
||||
- Anomalías que aparecen en TODOS los browsers → severity += 1 level
|
||||
- Anomalías que aparecen solo en un browser → añadir tag "browser-specific: webkit"
|
||||
|
||||
### Añadir a IAnomaly
|
||||
```typescript
|
||||
browser: 'chromium' | 'firefox' | 'webkit';
|
||||
browserVersion: string;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mobile Viewport Emulation
|
||||
|
||||
### Devices predefinidos (usar Playwright devices)
|
||||
```typescript
|
||||
type MobileDevice =
|
||||
| 'iPhone 14'
|
||||
| 'iPhone 14 Pro Max'
|
||||
| 'Pixel 7'
|
||||
| 'Galaxy S23'
|
||||
| 'iPad Pro'
|
||||
| 'none' // desktop (default)
|
||||
```
|
||||
|
||||
### En ExplorationConfig
|
||||
```typescript
|
||||
mobileDevice: MobileDevice; // default: 'none'
|
||||
viewport: { width: number; height: number } | null; // override manual
|
||||
```
|
||||
|
||||
### Implementación en PlaywrightAgent
|
||||
```typescript
|
||||
// Si mobileDevice !== 'none':
|
||||
const device = playwright.devices[config.mobileDevice];
|
||||
const context = await browser.newContext({ ...device });
|
||||
```
|
||||
|
||||
### Anomalías específicas de mobile
|
||||
Añadir tipo: `mobile_layout_issue` — detectado cuando:
|
||||
- Un elemento clickable tiene menos de 44x44px (WCAG touch target)
|
||||
- Hay scroll horizontal inesperado (viewport overflow)
|
||||
- Un elemento está fuera del viewport en mobile
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Testing (axe-core)
|
||||
|
||||
### Librería
|
||||
Usar `@axe-core/playwright` (integración oficial axe + Playwright).
|
||||
|
||||
### Cuándo ejecutar
|
||||
Después de cada acción que cambia el estado (navigation + click que resulta en nuevo estado).
|
||||
NO ejecutar en cada acción fill (demasiado frecuente).
|
||||
|
||||
### Implementación
|
||||
```typescript
|
||||
import { checkA11y } from 'axe-playwright';
|
||||
|
||||
// En PlaywrightAgent, después de captureState():
|
||||
async function runAccessibilityCheck(page: Page): Promise<IAccessibilityResult[]> {
|
||||
const results = await checkA11y(page, undefined, {
|
||||
detailedReport: true,
|
||||
detailedReportOptions: { html: true },
|
||||
});
|
||||
return results.violations.map(v => ({
|
||||
id: v.id,
|
||||
impact: v.impact, // 'minor' | 'moderate' | 'serious' | 'critical'
|
||||
description: v.description,
|
||||
helpUrl: v.helpUrl,
|
||||
nodes: v.nodes.length,
|
||||
selector: v.nodes[0]?.target?.join(', '),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Nuevo tipo de anomalía
|
||||
- type: `accessibility_violation`
|
||||
- severity mapping desde axe impact:
|
||||
- minor → low
|
||||
- moderate → medium
|
||||
- serious → high
|
||||
- critical → critical
|
||||
- description: "[axe] {violation.description}"
|
||||
- evidence: { helpUrl, affectedNodes, wcagCriteria }
|
||||
|
||||
### En ExplorationConfig
|
||||
```typescript
|
||||
accessibility: {
|
||||
enabled: boolean; // default: true
|
||||
minImpact: 'minor' | 'moderate' | 'serious' | 'critical'; // default: 'serious'
|
||||
wcagLevel: 'A' | 'AA' | 'AAA'; // default: 'AA'
|
||||
}
|
||||
```
|
||||
|
||||
### En el bug report
|
||||
Añadir sección "Accessibility Violations" en report.md con:
|
||||
- Lista de violaciones con impact badge
|
||||
- Link a la documentación de cada regla (helpUrl de axe)
|
||||
- Selector CSS del elemento afectado
|
||||
88
.ralph/specs/legacy/network-chaos.md
Normal file
88
.ralph/specs/legacy/network-chaos.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# ABE — Network Chaos Specification
|
||||
|
||||
## Concepto
|
||||
Inspirado en Gremlin y LitmusChaos, pero aplicado a nivel de browser.
|
||||
ABE puede simular condiciones de red adversas durante la exploración
|
||||
para descubrir cómo se comporta el app en redes lentas, intermitentes,
|
||||
o con servicios externos fallando.
|
||||
|
||||
## Esto es diferente al fuzzing de inputs:
|
||||
- Fuzzing: inputs inválidos en formularios
|
||||
- Network chaos: condiciones de red adversas (latencia, pérdida de paquetes, timeout)
|
||||
|
||||
## Implementación via Playwright CDP
|
||||
|
||||
Playwright expone Chrome DevTools Protocol (CDP) que permite controlar la red:
|
||||
```typescript
|
||||
// En PlaywrightAgent
|
||||
async function applyNetworkCondition(condition: NetworkCondition): Promise<void> {
|
||||
const client = await this.page.context().newCDPSession(this.page);
|
||||
await client.send('Network.emulateNetworkConditions', {
|
||||
offline: condition.offline,
|
||||
downloadThroughput: condition.downloadKbps * 1024 / 8,
|
||||
uploadThroughput: condition.uploadKbps * 1024 / 8,
|
||||
latency: condition.latencyMs,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Perfiles de red predefinidos
|
||||
```typescript
|
||||
const NETWORK_PROFILES = {
|
||||
'fast-3g': { downloadKbps: 1500, uploadKbps: 750, latencyMs: 40, offline: false },
|
||||
'slow-3g': { downloadKbps: 400, uploadKbps: 150, latencyMs: 400, offline: false },
|
||||
'2g': { downloadKbps: 50, uploadKbps: 30, latencyMs: 800, offline: false },
|
||||
'offline': { downloadKbps: 0, uploadKbps: 0, latencyMs: 0, offline: true },
|
||||
'none': null // sin limitación (default)
|
||||
}
|
||||
```
|
||||
|
||||
## API request interception (simular servicios caídos)
|
||||
```typescript
|
||||
// Simular que un endpoint específico falla con 503
|
||||
await page.route('**/api/payment**', route => {
|
||||
route.fulfill({ status: 503, body: 'Service Unavailable' });
|
||||
});
|
||||
|
||||
// Simular latencia en un endpoint específico
|
||||
await page.route('**/api/search**', async route => {
|
||||
await new Promise(r => setTimeout(r, 3000)); // 3s delay
|
||||
route.continue();
|
||||
});
|
||||
```
|
||||
|
||||
## Configuración en ExplorationConfig
|
||||
```typescript
|
||||
networkChaos: {
|
||||
enabled: boolean; // default: false
|
||||
profile: keyof typeof NETWORK_PROFILES; // default: 'none'
|
||||
blockedEndpoints: string[]; // glob patterns — responden 503
|
||||
slowEndpoints: Array<{
|
||||
pattern: string; // glob
|
||||
delayMs: number;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
## Anomalías específicas de network chaos
|
||||
|
||||
Añadir tipos al AnomalyDetector:
|
||||
|
||||
- `offline_handling_missing` — app muestra pantalla en blanco o error no controlado cuando está offline
|
||||
- `slow_network_no_feedback` — con slow-3g, la app no muestra loading indicator (detectado si CLS=0 pero LCP>5000ms y no hay elemento con rol 'progressbar' o 'status')
|
||||
- `external_service_crash` — cuando un endpoint bloqueado causa error 500 en el frontend
|
||||
|
||||
## Integración con el flujo de exploración
|
||||
|
||||
NetworkChaos se aplica de forma secuencial, no simultánea:
|
||||
1. Primera pasada: exploración normal (baseline)
|
||||
2. Segunda pasada (si networkChaos.enabled): misma seed, con perfil de red aplicado
|
||||
3. Comparar resultados: nuevas anomalías que aparecen solo en la segunda pasada son network-related
|
||||
|
||||
## Frontend — Network Chaos Config
|
||||
|
||||
En NewSessionForm, añadir sección collapsible "Network Chaos":
|
||||
- Toggle "Enable network chaos"
|
||||
- Select perfil: Fast 3G / Slow 3G / 2G / Offline
|
||||
- Textarea "Blocked endpoints" (uno por línea, glob patterns)
|
||||
- Lista "Slow endpoints" con campo pattern + delay ms
|
||||
64
.ralph/specs/legacy/notifications.md
Normal file
64
.ralph/specs/legacy/notifications.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# ABE — Notifications Specification
|
||||
|
||||
## Purpose
|
||||
When ABE finds an anomaly autonomously, notify the team immediately.
|
||||
|
||||
## Supported Channels
|
||||
|
||||
### 1. Slack Webhook
|
||||
Environment variable: `ABE_SLACK_WEBHOOK_URL`
|
||||
|
||||
Payload sent to Slack on anomaly:detected:
|
||||
```json
|
||||
{
|
||||
"text": "🐛 ABE found a bug!",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*ABE Bug Report*\n*Severity:* 🔴 HIGH\n*Type:* http_error\n*Description:* Form returns HTTP 500 on empty email\n*Session:* sess_abc123\n*Target:* http://localhost:3000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": { "type": "plain_text", "text": "View Report" },
|
||||
"url": "http://localhost:5173/anomalies/anom_abc123"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Only send for severity: high or critical (configurable via `ABE_NOTIFY_MIN_SEVERITY`).
|
||||
|
||||
### 2. Generic Webhook
|
||||
Environment variable: `ABE_WEBHOOK_URL`
|
||||
|
||||
POST request with the full IAnomaly object as JSON body.
|
||||
Includes header: `X-ABE-Event: anomaly.detected`
|
||||
|
||||
## Implementation
|
||||
|
||||
Create `src/server/notifications/`:
|
||||
- `NotificationService.ts` — main service, called after anomaly is persisted to DB
|
||||
- `SlackNotifier.ts` — implements Slack webhook
|
||||
- `WebhookNotifier.ts` — implements generic webhook
|
||||
|
||||
NotificationService.notify(anomaly) is called from the API server
|
||||
after every anomaly:detected event from the engine.
|
||||
|
||||
## Configuration (environment variables)
|
||||
```
|
||||
ABE_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz
|
||||
ABE_WEBHOOK_URL=https://myapp.com/webhooks/abe
|
||||
ABE_NOTIFY_MIN_SEVERITY=high # low | medium | high | critical
|
||||
```
|
||||
|
||||
## Notification record
|
||||
Every notification attempt (success or failure) is saved to the notifications table in SQLite.
|
||||
Failed notifications are retried once after 60 seconds.
|
||||
130
.ralph/specs/legacy/output-format.md
Normal file
130
.ralph/specs/legacy/output-format.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# ABE — Output Format Specification
|
||||
|
||||
Cada anomalía genera DOS archivos en `reports/{anomaly-id}/`:
|
||||
|
||||
---
|
||||
|
||||
## 1. report.json — Para consumo por AI y tooling
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"generated_at": "2025-01-15T10:30:00.000Z",
|
||||
"environment": {
|
||||
"target_url": "http://localhost:3000",
|
||||
"abe_version": "0.1.0",
|
||||
"os": "linux",
|
||||
"node_version": "20.x"
|
||||
},
|
||||
"anomaly": {
|
||||
"id": "anom_a1b2c3d4",
|
||||
"type": "http_error",
|
||||
"severity": "high",
|
||||
"description": "Form submission returns HTTP 500 on empty email field",
|
||||
"timestamp": 1705312200000
|
||||
},
|
||||
"reproduction": {
|
||||
"seed": 42,
|
||||
"steps": [
|
||||
{
|
||||
"step": 1,
|
||||
"action_type": "navigate",
|
||||
"url": "http://localhost:3000/register",
|
||||
"timestamp": 1705312195000
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"action_type": "fill",
|
||||
"selector": "input[name='email']",
|
||||
"value": "",
|
||||
"timestamp": 1705312196000
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"action_type": "click",
|
||||
"selector": "button[type='submit']",
|
||||
"timestamp": 1705312197000
|
||||
}
|
||||
]
|
||||
},
|
||||
"evidence": {
|
||||
"screenshot": "screenshot.png",
|
||||
"dom_snapshot": "dom.html",
|
||||
"http_log": [
|
||||
{
|
||||
"url": "http://localhost:3000/api/register",
|
||||
"method": "POST",
|
||||
"status": 500,
|
||||
"duration_ms": 234
|
||||
}
|
||||
],
|
||||
"console_errors": [],
|
||||
"js_exceptions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. report.md — Para lectura humana
|
||||
|
||||
El archivo Markdown debe tener exactamente esta estructura:
|
||||
```markdown
|
||||
# Bug Report — [tipo de anomalía] — [fecha]
|
||||
|
||||
## Summary
|
||||
[Una frase describiendo qué pasó y dónde]
|
||||
|
||||
## Severity
|
||||
[low | medium | high | critical] — [justificación en una frase]
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
1. Navigate to `[url]`
|
||||
2. [acción 2]
|
||||
3. [acción 3]
|
||||
...
|
||||
|
||||
**Seed used**: `42`
|
||||
**Replay command**: `npm run replay -- --report reports/anom_a1b2c3d4/report.json`
|
||||
|
||||
## Observed Behavior
|
||||
[Qué ocurrió exactamente — errores, respuestas HTTP, mensajes]
|
||||
|
||||
## Evidence
|
||||
- Screenshot: `reports/anom_a1b2c3d4/screenshot.png`
|
||||
- DOM Snapshot: `reports/anom_a1b2c3d4/dom.html`
|
||||
- HTTP Log: [tabla con las requests relevantes]
|
||||
|
||||
## Raw Errors
|
||||
\`\`\`
|
||||
[errores textuales tal cual aparecieron]
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estructura de carpetas de salida
|
||||
```
|
||||
reports/
|
||||
└── anom_a1b2c3d4/
|
||||
├── report.json ← estructurado para AI
|
||||
├── report.md ← legible para humanos
|
||||
├── screenshot.png ← captura en el momento de la anomalía
|
||||
└── dom.html ← snapshot completo del DOM
|
||||
|
||||
logs/
|
||||
└── session_20250115_103000.jsonl ← una línea JSON por evento
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Formato del log de sesión (.jsonl)
|
||||
|
||||
Cada línea es un objeto JSON independiente:
|
||||
```jsonl
|
||||
{"event":"session_start","timestamp":1705312190000,"seed":42,"target":"http://localhost:3000"}
|
||||
{"event":"state_discovered","timestamp":1705312191000,"state_id":"s_abc123","url":"/","title":"Home"}
|
||||
{"event":"action_executed","timestamp":1705312196000,"action_id":"act_xyz","type":"fill","selector":"input[name='email']","value":""}
|
||||
{"event":"anomaly_detected","timestamp":1705312197000,"anomaly_id":"anom_a1b2c3d4","type":"http_error","severity":"high"}
|
||||
{"event":"session_end","timestamp":1705312210000,"states_visited":3,"anomalies_found":1}
|
||||
```
|
||||
124
.ralph/specs/legacy/performance-metrics.md
Normal file
124
.ralph/specs/legacy/performance-metrics.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ABE — Performance Metrics Specification
|
||||
|
||||
## Concepto
|
||||
Durante la exploración, ABE captura métricas de rendimiento de cada
|
||||
estado visitado. Inspirado en Checkly y Datadog RUM.
|
||||
Esto permite detectar anomalías de rendimiento además de errores funcionales.
|
||||
|
||||
## Métricas capturadas por estado
|
||||
```typescript
|
||||
interface IPerformanceMetrics {
|
||||
stateId: string;
|
||||
url: string;
|
||||
timestamp: number;
|
||||
|
||||
// Navigation Timing (disponibles via Playwright)
|
||||
ttfb: number; // Time to First Byte (ms)
|
||||
domContentLoaded: number; // DOMContentLoaded event (ms)
|
||||
loadComplete: number; // Load event (ms)
|
||||
|
||||
// Core Web Vitals (via web-vitals library injected)
|
||||
lcp: number | null; // Largest Contentful Paint (ms)
|
||||
cls: number | null; // Cumulative Layout Shift (score)
|
||||
fid: number | null; // First Input Delay (ms) - solo tras interacción
|
||||
inp: number | null; // Interaction to Next Paint (ms)
|
||||
|
||||
// Resource counts
|
||||
totalRequests: number;
|
||||
failedRequests: number;
|
||||
totalTransferSize: number; // bytes
|
||||
}
|
||||
```
|
||||
|
||||
## Implementación
|
||||
|
||||
### TTFB, DOMContentLoaded, Load
|
||||
Via `page.evaluate()` usando `performance.timing` después de navigation:
|
||||
```typescript
|
||||
const timing = await page.evaluate(() => ({
|
||||
ttfb: performance.timing.responseStart - performance.timing.requestStart,
|
||||
domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
|
||||
loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
|
||||
}));
|
||||
```
|
||||
|
||||
### Core Web Vitals
|
||||
Inyectar el script de `web-vitals` (npm) en la página:
|
||||
```typescript
|
||||
await page.addScriptTag({ url: 'https://unpkg.com/web-vitals/dist/web-vitals.iife.js' });
|
||||
const vitals = await page.evaluate(() => new Promise(resolve => {
|
||||
const result = {};
|
||||
webVitals.getLCP(m => result.lcp = m.value);
|
||||
webVitals.getCLS(m => result.cls = m.value);
|
||||
webVitals.getINP(m => result.inp = m.value);
|
||||
setTimeout(() => resolve(result), 3000); // wait 3s for vitals
|
||||
}));
|
||||
```
|
||||
|
||||
## Anomalías de rendimiento (nuevos tipos)
|
||||
|
||||
Añadir al AnomalyDetector con umbrales basados en Core Web Vitals de Google:
|
||||
|
||||
| Métrica | Good | Needs Improvement | Poor (anomalía) |
|
||||
|---------|---------|-------------------|-----------------|
|
||||
| LCP | <2500ms | 2500-4000ms | >4000ms → high |
|
||||
| CLS | <0.1 | 0.1-0.25 | >0.25 → medium |
|
||||
| INP | <200ms | 200-500ms | >500ms → high |
|
||||
| TTFB | <800ms | 800-1800ms | >1800ms → medium|
|
||||
|
||||
Tipo de anomalía: `performance_degradation`
|
||||
|
||||
## Modelo de datos — añadir a SQLite
|
||||
|
||||
### Table: performance_metrics
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS performance_metrics (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
state_id TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
ttfb INTEGER,
|
||||
dom_content_loaded INTEGER,
|
||||
load_complete INTEGER,
|
||||
lcp INTEGER,
|
||||
cls REAL,
|
||||
fid INTEGER,
|
||||
inp INTEGER,
|
||||
total_requests INTEGER,
|
||||
failed_requests INTEGER,
|
||||
total_transfer_size INTEGER,
|
||||
captured_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## Frontend — Performance tab
|
||||
|
||||
Añadir tab "Performance" en SessionDetail:
|
||||
- Tabla con todos los estados visitados y sus métricas
|
||||
- Columnas con color coded: verde/amarillo/rojo según umbrales de Google
|
||||
- Gráfico de barras: LCP por estado (para identificar páginas lentas)
|
||||
- Summary cards: peor LCP, peor CLS, peor TTFB de la sesión
|
||||
|
||||
## En el bug report
|
||||
|
||||
Si hay anomalía performance_degradation, añadir sección en report.md:
|
||||
```
|
||||
## Performance Issue
|
||||
- LCP: 5200ms (threshold: 4000ms) ❌
|
||||
- CLS: 0.08 ✅
|
||||
- TTFB: 2100ms (threshold: 1800ms) ❌
|
||||
- Total page size: 4.2MB
|
||||
```
|
||||
|
||||
## Configuración
|
||||
|
||||
Añadir a ExplorationConfig:
|
||||
```typescript
|
||||
performance: {
|
||||
enabled: boolean; // default: true
|
||||
lcpThresholdMs: number; // default: 4000
|
||||
clsThreshold: number; // default: 0.25
|
||||
inpThresholdMs: number; // default: 500
|
||||
ttfbThresholdMs: number; // default: 1800
|
||||
}
|
||||
```
|
||||
77
.ralph/specs/legacy/production-hardening.md
Normal file
77
.ralph/specs/legacy/production-hardening.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# ABE — Production Hardening Specification
|
||||
|
||||
## Health Endpoints (no auth required)
|
||||
|
||||
### GET /health
|
||||
Returns 200 if server is up.
|
||||
```json
|
||||
{ "status": "ok", "version": "0.1.0", "uptime_seconds": 3600 }
|
||||
```
|
||||
|
||||
### GET /ready
|
||||
Returns 200 if server is ready to accept requests (DB connected, no critical errors).
|
||||
Returns 503 if not ready.
|
||||
```json
|
||||
{ "status": "ready", "db": "connected", "active_sessions": 2 }
|
||||
```
|
||||
|
||||
Used by Docker HEALTHCHECK and Kubernetes readiness probes.
|
||||
|
||||
## Docker improvements
|
||||
|
||||
### Backend Dockerfile
|
||||
Add HEALTHCHECK:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:3001/health || exit 1
|
||||
```
|
||||
|
||||
### docker-compose.yml updates
|
||||
- Add healthcheck to backend service
|
||||
- Add `restart: unless-stopped` to both services
|
||||
- Add `data/` volume for SQLite persistence
|
||||
- Load `.env` file: `env_file: .env`
|
||||
- Add `depends_on: backend: condition: service_healthy` to frontend
|
||||
|
||||
### .env.example file
|
||||
Create `.env.example` in repo root with all variables and example values.
|
||||
`.env` added to `.gitignore`.
|
||||
|
||||
## Error handling improvements
|
||||
|
||||
Global Express error handler in `src/server/index.ts`:
|
||||
- Catch all unhandled errors
|
||||
- Log with timestamp and stack trace
|
||||
- Return consistent JSON error format:
|
||||
```json
|
||||
{ "error": "Internal server error", "code": "INTERNAL_ERROR", "timestamp": 1705312200000 }
|
||||
```
|
||||
|
||||
Never expose stack traces in production (NODE_ENV=production).
|
||||
|
||||
## Graceful shutdown
|
||||
|
||||
On SIGTERM/SIGINT:
|
||||
1. Stop accepting new sessions
|
||||
2. Wait for active sessions to finish (max 30s)
|
||||
3. Close DB connection
|
||||
4. Exit 0
|
||||
|
||||
## Concurrency limits
|
||||
|
||||
- Max concurrent exploration sessions: configurable via `ABE_MAX_CONCURRENT_SESSIONS` (default: 3)
|
||||
- If limit reached, POST /api/sessions returns 429 with:
|
||||
```json
|
||||
{ "error": "Max concurrent sessions reached", "active": 3, "limit": 3 }
|
||||
```
|
||||
|
||||
## Logging improvements
|
||||
|
||||
Replace console.log with structured logger (use `pino`):
|
||||
```typescript
|
||||
log.info({ sessionId, url, event: 'session_started' }, 'Session started')
|
||||
log.error({ anomalyId, error }, 'Failed to capture screenshot')
|
||||
```
|
||||
|
||||
All logs go to stdout (Docker captures them).
|
||||
Log level configurable via `ABE_LOG_LEVEL` env var (default: 'info').
|
||||
138
.ralph/specs/legacy/project-structure.md
Normal file
138
.ralph/specs/legacy/project-structure.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# ABE — Project Structure Specification
|
||||
|
||||
## Árbol completo de archivos a crear
|
||||
```
|
||||
abe/
|
||||
├── src/
|
||||
│ ├── core/
|
||||
│ │ ├── interfaces.ts ← TODAS las interfaces (IState, IAction, etc.)
|
||||
│ │ ├── StateGraph.ts ← implementación del grafo de estados
|
||||
│ │ ├── ExplorationEngine.ts ← loop principal de exploración
|
||||
│ │ └── AnomalyDetector.ts ← reglas heurísticas de detección
|
||||
│ ├── plugins/
|
||||
│ │ ├── agents/
|
||||
│ │ │ └── PlaywrightAgent.ts ← implementa IInteractionAgent
|
||||
│ │ ├── collectors/
|
||||
│ │ │ ├── ScreenshotCollector.ts
|
||||
│ │ │ ├── NetworkCollector.ts
|
||||
│ │ │ └── DOMSnapshotCollector.ts
|
||||
│ │ ├── exporters/
|
||||
│ │ │ ├── MarkdownExporter.ts
|
||||
│ │ │ └── JSONExporter.ts
|
||||
│ │ └── reproducers/
|
||||
│ │ └── PlaywrightReproducer.ts
|
||||
│ └── index.ts ← punto de entrada, conecta todo
|
||||
│
|
||||
├── tests/
|
||||
│ ├── core/
|
||||
│ │ ├── StateGraph.test.ts
|
||||
│ │ ├── ExplorationEngine.test.ts
|
||||
│ │ └── AnomalyDetector.test.ts
|
||||
│ └── plugins/
|
||||
│ ├── agents/
|
||||
│ │ └── PlaywrightAgent.test.ts
|
||||
│ └── exporters/
|
||||
│ ├── MarkdownExporter.test.ts
|
||||
│ └── JSONExporter.test.ts
|
||||
│
|
||||
├── reports/ ← generado en runtime, ignorado por git
|
||||
├── logs/ ← generado en runtime, ignorado por git
|
||||
│
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── jest.config.ts
|
||||
└── CLAUDE.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de importación — MUY IMPORTANTE
|
||||
```
|
||||
✅ PERMITIDO:
|
||||
src/core/ExplorationEngine.ts → importa de src/core/interfaces.ts
|
||||
src/plugins/agents/PlaywrightAgent.ts → importa de src/core/interfaces.ts
|
||||
src/index.ts → importa de src/core/ Y src/plugins/
|
||||
|
||||
❌ PROHIBIDO:
|
||||
src/core/ExplorationEngine.ts → importa de src/plugins/ (rompe el desacoplamiento)
|
||||
src/plugins/agents/A.ts → importa de src/plugins/exporters/B.ts (plugins no se conocen entre sí)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cómo se conecta todo en src/index.ts
|
||||
|
||||
El archivo de entrada debe seguir este patrón:
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { ExplorationEngine } from './core/ExplorationEngine';
|
||||
import { StateGraph } from './core/StateGraph';
|
||||
import { PlaywrightAgent } from './plugins/agents/PlaywrightAgent';
|
||||
import { ScreenshotCollector } from './plugins/collectors/ScreenshotCollector';
|
||||
import { NetworkCollector } from './plugins/collectors/NetworkCollector';
|
||||
import { DOMSnapshotCollector } from './plugins/collectors/DOMSnapshotCollector';
|
||||
import { JSONExporter } from './plugins/exporters/JSONExporter';
|
||||
import { MarkdownExporter } from './plugins/exporters/MarkdownExporter';
|
||||
import { PlaywrightReproducer } from './plugins/reproducers/PlaywrightReproducer';
|
||||
|
||||
const graph = new StateGraph();
|
||||
const agent = new PlaywrightAgent();
|
||||
const collectors = [new ScreenshotCollector(), new NetworkCollector(), new DOMSnapshotCollector()];
|
||||
const exporters = [new JSONExporter(), new MarkdownExporter()];
|
||||
const reproducer = new PlaywrightReproducer();
|
||||
|
||||
const engine = new ExplorationEngine({ graph, agent, collectors, exporters, reproducer });
|
||||
|
||||
engine.run({ url: process.argv[2] || 'http://localhost:3000', seed: 42 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## package.json — scripts obligatorios
|
||||
```json
|
||||
{
|
||||
"name": "abe",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src/ tests/",
|
||||
"explore": "ts-node src/index.ts",
|
||||
"replay": "ts-node src/replay.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## tsconfig.json — configuración base
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## jest.config.ts — configuración base
|
||||
```typescript
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
};
|
||||
```
|
||||
79
.ralph/specs/legacy/scheduled-monitoring.md
Normal file
79
.ralph/specs/legacy/scheduled-monitoring.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# ABE — Scheduled Monitoring Specification
|
||||
|
||||
## Concepto
|
||||
ABE puede ejecutar exploraciones de forma automática en intervalos definidos,
|
||||
sin intervención humana. Esto convierte ABE de una herramienta manual
|
||||
en un sistema de monitorización continua, al estilo Checkly.
|
||||
|
||||
## Modelo de datos — añadir a SQLite
|
||||
|
||||
### Table: schedules
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
cron_expression TEXT NOT NULL, -- e.g. "0 */6 * * *" (every 6h)
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_run_at INTEGER,
|
||||
next_run_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## Expresiones cron soportadas (presets en la UI)
|
||||
|
||||
| Label | Cron |
|
||||
|------------------|----------------|
|
||||
| Every 15 minutes | */15 * * * * |
|
||||
| Every hour | 0 * * * * |
|
||||
| Every 6 hours | 0 */6 * * * |
|
||||
| Every day at 2am | 0 2 * * * |
|
||||
| Every Monday 9am | 0 9 * * 1 |
|
||||
|
||||
## Implementación
|
||||
|
||||
Usar `node-cron` para el scheduler.
|
||||
Crear `src/server/scheduler/SchedulerService.ts`:
|
||||
- En startup, carga todos los schedules con enabled=1 de la DB
|
||||
- Registra un cron job por cada schedule
|
||||
- Cuando dispara, llama internamente a POST /api/sessions con la config guardada
|
||||
- Actualiza last_run_at y next_run_at en la DB después de cada disparo
|
||||
- Si la sesión anterior sigue running, skip este tick y log warning
|
||||
|
||||
## API endpoints nuevos
|
||||
|
||||
### GET /api/schedules
|
||||
Lista todos los schedules.
|
||||
|
||||
### POST /api/schedules
|
||||
Crea un nuevo schedule.
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"name": "Production daily check",
|
||||
"url": "https://myapp.com",
|
||||
"config": { ... mismo ExplorationConfig ... },
|
||||
"cronExpression": "0 2 * * *",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH /api/schedules/:id
|
||||
Actualiza o activa/desactiva un schedule.
|
||||
|
||||
### DELETE /api/schedules/:id
|
||||
Elimina un schedule.
|
||||
|
||||
## Frontend — nueva sección en Settings
|
||||
|
||||
Añadir tab "Schedules" en /settings:
|
||||
- Lista de schedules activos con: nombre, URL, cron, última ejecución, próxima ejecución, toggle activo/inactivo
|
||||
- Botón "New Schedule" abre modal con: nombre, URL, config de exploración, selector de frecuencia (presets + custom cron)
|
||||
- Badge "Running" si hay una sesión activa del schedule en este momento
|
||||
|
||||
## Notificaciones específicas de schedules
|
||||
|
||||
Cuando un schedule dispara una exploración y encuentra anomalías high/critical,
|
||||
enviar notificación con el subject: "[SCHEDULED] ABE found bugs in {url}"
|
||||
124
.ralph/specs/legacy/visual-regression.md
Normal file
124
.ralph/specs/legacy/visual-regression.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ABE — Visual Regression Testing Specification
|
||||
|
||||
## Concepto
|
||||
ABE toma screenshots durante la exploración. En vez de solo guardarlos,
|
||||
los compara contra una baseline aprobada para detectar cambios visuales
|
||||
inesperados entre ejecuciones. Inspirado en Percy y Chromatic,
|
||||
pero integrado directamente en el flujo de exploración autónoma.
|
||||
|
||||
## Cómo funciona
|
||||
|
||||
### Primera ejecución (sin baseline)
|
||||
1. ABE explora el app, toma screenshots de cada estado descubierto
|
||||
2. Todos los screenshots se marcan como "pending review" en la UI
|
||||
3. El usuario aprueba o rechaza cada uno desde la GUI
|
||||
4. Los aprobados se convierten en la BASELINE
|
||||
|
||||
### Ejecuciones posteriores
|
||||
1. ABE explora el app, toma screenshots de cada estado
|
||||
2. Para cada screenshot, busca la baseline correspondiente por state_id (hash DOM+URL)
|
||||
3. Si no hay baseline: marcar como "new state", notificar
|
||||
4. Si hay baseline: comparar usando pixelmatch (npm library)
|
||||
5. Si diff > threshold (default 0.1%): crear anomalía tipo visual_regression
|
||||
6. Si diff <= threshold: marcar como "passed"
|
||||
|
||||
## Librería de comparación
|
||||
|
||||
Usar `pixelmatch` (npm) para comparación pixel a pixel.
|
||||
Usar `sharp` para resize y normalización de imágenes antes de comparar.
|
||||
```typescript
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import sharp from 'sharp';
|
||||
|
||||
async function compareScreenshots(
|
||||
baselinePath: string,
|
||||
currentPath: string,
|
||||
diffOutputPath: string,
|
||||
threshold: number = 0.1
|
||||
): Promise<{ diffPixels: number; diffPercent: number; hasDiff: boolean }> {
|
||||
// resize both to same dimensions, compare, generate diff image
|
||||
}
|
||||
```
|
||||
|
||||
## Modelo de datos — añadir a SQLite
|
||||
|
||||
### Table: visual_baselines
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS visual_baselines (
|
||||
id TEXT PRIMARY KEY,
|
||||
state_id TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
screenshot_path TEXT NOT NULL,
|
||||
approved_at INTEGER NOT NULL,
|
||||
approved_by TEXT DEFAULT 'user',
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Table: visual_comparisons
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS visual_comparisons (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
state_id TEXT NOT NULL,
|
||||
baseline_id TEXT,
|
||||
current_screenshot_path TEXT NOT NULL,
|
||||
diff_screenshot_path TEXT,
|
||||
diff_pixels INTEGER,
|
||||
diff_percent REAL,
|
||||
status TEXT NOT NULL, -- 'passed' | 'failed' | 'new_state' | 'pending'
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## Nuevo tipo de anomalía
|
||||
|
||||
Añadir a AnomalyDetector:
|
||||
- type: `visual_regression`
|
||||
- severity: calculado por diff_percent:
|
||||
- < 1% → low
|
||||
- 1-5% → medium
|
||||
- 5-15% → high
|
||||
- > 15% → critical
|
||||
- description: "Visual regression detected: X% of pixels changed"
|
||||
- evidence: baseline screenshot + current screenshot + diff image (highlighted in red)
|
||||
|
||||
## Nuevo endpoint de API
|
||||
|
||||
### GET /api/visual/comparisons
|
||||
Lista todas las comparaciones de la sesión más reciente.
|
||||
Query: ?status=failed&sessionId=xxx
|
||||
|
||||
### POST /api/visual/baselines/:comparisonId/approve
|
||||
Aprueba un screenshot como nueva baseline.
|
||||
|
||||
### POST /api/visual/baselines/:comparisonId/reject
|
||||
Rechaza (anomalía confirmada, no actualizar baseline).
|
||||
|
||||
### POST /api/visual/baselines/approve-all
|
||||
Aprueba todos los "new_state" pendientes de una sesión.
|
||||
|
||||
## Frontend — nueva sección Visual Review
|
||||
|
||||
Nueva página /visual-review:
|
||||
- Grid de cards, cada una muestra: URL del estado, thumbnail del screenshot actual
|
||||
- Filtros: passed | failed | new_state | pending
|
||||
- Click en una card abre modal con:
|
||||
- Vista lado a lado: baseline izquierda, actual derecha
|
||||
- Vista diff: imagen con píxeles cambiados en rojo
|
||||
- Porcentaje de cambio
|
||||
- Botones: Approve as new baseline | Mark as bug | Ignore
|
||||
- Bulk actions: "Approve all new states", "Mark all failed as bugs"
|
||||
|
||||
## Configuración
|
||||
|
||||
Añadir a ExplorationConfig:
|
||||
```typescript
|
||||
visualRegression: {
|
||||
enabled: boolean; // default: true
|
||||
threshold: number; // default: 0.001 (0.1%)
|
||||
screenshotFullPage: boolean; // default: false (solo viewport)
|
||||
ignoreSelectors: string[]; // e.g. [".timestamp", ".ad-banner"] — excluir zonas dinámicas
|
||||
}
|
||||
```
|
||||
134
.ralph/specs/phase-01-shared-domain.md
Normal file
134
.ralph/specs/phase-01-shared-domain.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Phase 1: Shared Domain — Building Blocks
|
||||
|
||||
## Objetivo
|
||||
Crear las clases base que TODOS los módulos usarán. Esto es el cimiento.
|
||||
|
||||
## Result.ts
|
||||
```typescript
|
||||
// Discriminated union, no classes
|
||||
type ResultOk<T> = { readonly ok: true; readonly value: T };
|
||||
type ResultErr<E> = { readonly ok: false; readonly error: E };
|
||||
export type Result<T, E = Error> = ResultOk<T> | ResultErr<E>;
|
||||
|
||||
export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
|
||||
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|
||||
export function isOk<T, E>(r: Result<T, E>): r is ResultOk<T> { return r.ok; }
|
||||
export function isErr<T, E>(r: Result<T, E>): r is ResultErr<E> { return !r.ok; }
|
||||
```
|
||||
|
||||
## UniqueId.ts
|
||||
```typescript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class UniqueId {
|
||||
private constructor(private readonly value: string) {}
|
||||
static create(): UniqueId { return new UniqueId(uuidv4()); }
|
||||
static from(value: string): UniqueId { return new UniqueId(value); }
|
||||
toString(): string { return this.value; }
|
||||
equals(other?: UniqueId): boolean {
|
||||
if (!other) return false;
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entity.ts
|
||||
```typescript
|
||||
export abstract class Entity<T> {
|
||||
protected readonly _id: UniqueId;
|
||||
protected props: T;
|
||||
|
||||
constructor(props: T, id?: UniqueId) {
|
||||
this._id = id ?? UniqueId.create();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
get id(): UniqueId { return this._id; }
|
||||
|
||||
equals(other?: Entity<T>): boolean {
|
||||
if (!other) return false;
|
||||
return this._id.equals(other._id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AggregateRoot.ts
|
||||
```typescript
|
||||
export abstract class AggregateRoot<T> extends Entity<T> {
|
||||
private _domainEvents: DomainEvent[] = [];
|
||||
|
||||
get domainEvents(): ReadonlyArray<DomainEvent> {
|
||||
return this._domainEvents;
|
||||
}
|
||||
|
||||
protected addDomainEvent(event: DomainEvent): void {
|
||||
this._domainEvents.push(event);
|
||||
}
|
||||
|
||||
clearEvents(): DomainEvent[] {
|
||||
const events = [...this._domainEvents];
|
||||
this._domainEvents = [];
|
||||
return events;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ValueObject.ts
|
||||
```typescript
|
||||
export abstract class ValueObject<T> {
|
||||
protected readonly props: T;
|
||||
|
||||
constructor(props: T) {
|
||||
this.props = Object.freeze(props);
|
||||
}
|
||||
|
||||
equals(other?: ValueObject<T>): boolean {
|
||||
if (!other) return false;
|
||||
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DomainEvent.ts
|
||||
```typescript
|
||||
export interface DomainEvent {
|
||||
readonly eventId: string;
|
||||
readonly eventName: string;
|
||||
readonly aggregateId: string;
|
||||
readonly occurredOn: Date;
|
||||
readonly payload: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
## UseCase.ts
|
||||
```typescript
|
||||
export interface UseCase<TRequest, TResponse, TError = Error> {
|
||||
execute(request: TRequest): Promise<Result<TResponse, TError>>;
|
||||
}
|
||||
```
|
||||
|
||||
## EventBus.ts + EventHandler.ts
|
||||
```typescript
|
||||
// EventBus.ts
|
||||
export interface EventBus {
|
||||
publish(event: DomainEvent): Promise<void>;
|
||||
subscribe(eventName: string, handler: EventHandler): void;
|
||||
}
|
||||
|
||||
// EventHandler.ts
|
||||
export interface EventHandler {
|
||||
handle(event: DomainEvent): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Tests requeridos (mínimo)
|
||||
1. Result: Ok crea value accesible, Err crea error accesible, isOk/isErr discriminan
|
||||
2. UniqueId: create genera string válido, equals funciona, from preserva valor
|
||||
3. Entity: equals compara por id (no por props)
|
||||
4. ValueObject: equals compara por props, props son inmutables
|
||||
|
||||
## IMPORTANTE
|
||||
- Estos archivos NO importan NADA externo excepto 'uuid'
|
||||
- NO usar decorators
|
||||
- NO usar classes abstractas complicadas — mantener simple
|
||||
- Cada archivo exporta UNA cosa principal
|
||||
136
.ralph/specs/phase-02-shared-infrastructure.md
Normal file
136
.ralph/specs/phase-02-shared-infrastructure.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Phase 2: Shared Infrastructure
|
||||
|
||||
## Config.ts
|
||||
Usa Zod para validar TODAS las env vars al arranque. Si falla → crash inmediato con mensaje claro.
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const configSchema = z.object({
|
||||
port: z.coerce.number().default(3001),
|
||||
host: z.string().default('0.0.0.0'),
|
||||
nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
|
||||
db: z.object({
|
||||
driver: z.enum(['sqlite', 'postgres']).default('sqlite'),
|
||||
path: z.string().default('./data/abe.db'),
|
||||
url: z.string().optional(),
|
||||
}),
|
||||
auth: z.object({
|
||||
secret: z.string().min(16).default('abe-dev-secret-change-in-prod'),
|
||||
sessionMaxAge: z.coerce.number().default(86400),
|
||||
}),
|
||||
storage: z.object({
|
||||
driver: z.enum(['local', 's3']).default('local'),
|
||||
path: z.string().default('./data/storage'),
|
||||
}),
|
||||
cors: z.object({ origin: z.string().default('http://localhost:5173') }),
|
||||
log: z.object({ level: z.enum(['debug','info','warn','error']).default('info') }),
|
||||
api: z.object({
|
||||
key: z.string().default('abe-dev-key-123'),
|
||||
rateLimitWindowMs: z.coerce.number().default(900000),
|
||||
rateLimitMax: z.coerce.number().default(100),
|
||||
}),
|
||||
ai: z.object({
|
||||
provider: z.enum(['claude','openai','ollama','none']).default('none'),
|
||||
apiKey: z.string().default(''),
|
||||
autoEnrich: z.coerce.boolean().default(false),
|
||||
minSeverity: z.enum(['low','medium','high','critical']).default('high'),
|
||||
}),
|
||||
jobs: z.object({
|
||||
maxConcurrentSessions: z.coerce.number().default(3),
|
||||
pollIntervalMs: z.coerce.number().default(1000),
|
||||
}),
|
||||
license: z.object({ key: z.string().default('') }),
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof configSchema>;
|
||||
|
||||
export function loadConfig(): AppConfig {
|
||||
// Map env vars to schema shape, parse
|
||||
}
|
||||
```
|
||||
|
||||
## Logger.ts
|
||||
```typescript
|
||||
import pino from 'pino';
|
||||
|
||||
export function createLogger(config: { level: string; nodeEnv: string }): pino.Logger {
|
||||
return pino({
|
||||
level: config.level,
|
||||
transport: config.nodeEnv === 'development'
|
||||
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
export type Logger = pino.Logger;
|
||||
```
|
||||
|
||||
## DatabaseConnection.ts
|
||||
```typescript
|
||||
import { Kysely, SqliteDialect } from 'kysely';
|
||||
import SQLite from 'better-sqlite3';
|
||||
|
||||
// Define Database interface con todas las tablas
|
||||
export interface Database {
|
||||
sessions: SessionTable;
|
||||
states: StateTable;
|
||||
actions: ActionTable;
|
||||
anomalies: AnomalyTable;
|
||||
// ... más tablas se añaden en fases posteriores
|
||||
}
|
||||
|
||||
export function createDatabase(config: { driver: string; path: string; url?: string }): Kysely<Database> {
|
||||
if (config.driver === 'postgres') {
|
||||
// Import dinámico de pg para no requerir en SQLite
|
||||
const { Pool } = require('pg');
|
||||
const { PostgresDialect } = require('kysely');
|
||||
return new Kysely<Database>({
|
||||
dialect: new PostgresDialect({ pool: new Pool({ connectionString: config.url }) }),
|
||||
});
|
||||
}
|
||||
|
||||
// Crear directorio data/ si no existe
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
fs.mkdirSync(path.dirname(config.path), { recursive: true });
|
||||
|
||||
return new Kysely<Database>({
|
||||
dialect: new SqliteDialect({ database: new SQLite(config.path) }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## InProcessEventBus.ts
|
||||
```typescript
|
||||
import { EventEmitter } from 'events';
|
||||
// Implements EventBus interface from shared/application
|
||||
// Logging de cada evento publicado
|
||||
// Catch errors en handlers (log pero no crash)
|
||||
// setMaxListeners(50)
|
||||
```
|
||||
|
||||
## StorageProvider.ts
|
||||
```typescript
|
||||
export interface IStorageProvider {
|
||||
save(relativePath: string, data: Buffer): Promise<string>;
|
||||
get(relativePath: string): Promise<Buffer | null>;
|
||||
delete(relativePath: string): Promise<void>;
|
||||
exists(relativePath: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
// LocalStorageProvider: usa fs.promises, base path = config.storage.path
|
||||
// Crea directorios automáticamente con mkdir recursive
|
||||
```
|
||||
|
||||
## Migración 001
|
||||
Crea las tablas que ya existen en el schema actual (sessions, states, actions, anomalies, notifications).
|
||||
Usar `CREATE TABLE IF NOT EXISTS` para idempotencia.
|
||||
Los tipos de columna deben coincidir con lo que ya tiene better-sqlite3.
|
||||
|
||||
## IMPORTANTE
|
||||
- Config DEBE fallar rápido si hay env vars inválidas
|
||||
- Logger NUNCA debe usar console.log
|
||||
- Database factory NUNCA importa pg a menos que driver sea postgres
|
||||
- EventBus handlers que fallan se loguean pero NO crashean el bus
|
||||
|
||||
216
.ralph/specs/phase-07-api-server.md
Normal file
216
.ralph/specs/phase-07-api-server.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Phase 7: API Server Refactor + Composition Root
|
||||
|
||||
## Middleware stack (ORDEN IMPORTA)
|
||||
```typescript
|
||||
// server.ts
|
||||
export function createServer(deps: ServerDependencies): Express {
|
||||
const app = express();
|
||||
|
||||
// 1. Request ID (PRIMERO — todo log necesita esto)
|
||||
app.use(requestIdMiddleware);
|
||||
|
||||
// 2. Security headers
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: ["'self'", "ws:", "wss:"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"], // para Scalar docs
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// 3. CORS
|
||||
app.use(cors({
|
||||
origin: deps.config.cors.origin,
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// 4. Rate limiting global
|
||||
app.use(rateLimit({
|
||||
windowMs: deps.config.api.rateLimitWindowMs,
|
||||
max: deps.config.api.rateLimitMax,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}));
|
||||
|
||||
// 5. Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// 6. Health endpoints (SIN auth)
|
||||
app.get('/health/live', (_, res) => res.json({ status: 'ok' }));
|
||||
app.get('/health/ready', async (_, res) => { /* check DB */ });
|
||||
|
||||
// 7. Auth routes (SIN auth middleware general)
|
||||
app.use('/api/auth', deps.authController.router);
|
||||
|
||||
// 8. Auth middleware (TODOS los /api/ a partir de aquí)
|
||||
app.use('/api', deps.authMiddleware);
|
||||
|
||||
// 9. Module routes
|
||||
app.use('/api', deps.crawlingController.router);
|
||||
app.use('/api', deps.findingsController.router);
|
||||
app.use('/api', deps.fuzzingController.router);
|
||||
// ... más módulos
|
||||
|
||||
// 10. 404 handler
|
||||
app.use(notFoundMiddleware);
|
||||
|
||||
// 11. Error handler (SIEMPRE último)
|
||||
app.use(globalErrorHandler);
|
||||
|
||||
return app;
|
||||
}
|
||||
```
|
||||
|
||||
## Error hierarchy
|
||||
```typescript
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode: number,
|
||||
public readonly code: string,
|
||||
public readonly isOperational = true,
|
||||
) { super(message); }
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, public readonly details?: unknown) {
|
||||
super(message, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
}
|
||||
export class AuthenticationError extends AppError {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message, 401, 'AUTHENTICATION_ERROR');
|
||||
}
|
||||
}
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message = 'Forbidden') {
|
||||
super(message, 403, 'FORBIDDEN');
|
||||
}
|
||||
}
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(resource: string) {
|
||||
super(`${resource} not found`, 404, 'NOT_FOUND');
|
||||
}
|
||||
}
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message, 409, 'CONFLICT');
|
||||
}
|
||||
}
|
||||
export class RateLimitError extends AppError {
|
||||
constructor() {
|
||||
super('Too many requests', 429, 'RATE_LIMIT');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global error handler
|
||||
```typescript
|
||||
export function globalErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
||||
const logger = req.log || console; // pino child logger
|
||||
|
||||
if (err instanceof AppError && err.isOperational) {
|
||||
logger.warn({ err, statusCode: err.statusCode }, err.message);
|
||||
return res.status(err.statusCode).json({
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
...(err instanceof ValidationError && err.details ? { details: err.details } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Programmer error — log full stack, return generic message
|
||||
logger.error({ err }, 'Unhandled error');
|
||||
return res.status(500).json({
|
||||
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
|
||||
code: 'INTERNAL_ERROR',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Composition root (main.ts)
|
||||
```typescript
|
||||
async function bootstrap() {
|
||||
// 1. Config
|
||||
const config = loadConfig();
|
||||
|
||||
// 2. Logger
|
||||
const logger = createLogger(config);
|
||||
logger.info({ port: config.port }, 'Starting ABE...');
|
||||
|
||||
// 3. Database + migrations
|
||||
const db = createDatabase(config.db);
|
||||
await runMigrations(db, logger);
|
||||
|
||||
// 4. Event bus
|
||||
const eventBus = new InProcessEventBus(logger);
|
||||
|
||||
// 5. Storage
|
||||
const storage = new LocalStorageProvider(config.storage.path);
|
||||
|
||||
// 6. Repositories
|
||||
const sessionRepo = new KyselyCrawlSessionRepository(db);
|
||||
const findingRepo = new KyselyFindingRepository(db);
|
||||
// ... etc
|
||||
|
||||
// 7. Use cases
|
||||
const startCrawl = new StartCrawlCommand(sessionRepo, eventBus);
|
||||
const listFindings = new ListFindingsQuery(findingRepo);
|
||||
// ... etc
|
||||
|
||||
// 8. Event handlers — subscribe to event bus
|
||||
const onAnomalyDetected = new OnAnomalyDetected(findingRepo, eventBus);
|
||||
eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected);
|
||||
// ... etc
|
||||
|
||||
// 9. Controllers
|
||||
const crawlingController = new CrawlingController(startCrawl, ...);
|
||||
const findingsController = new FindingsController(listFindings, ...);
|
||||
// ... etc
|
||||
|
||||
// 10. HTTP server
|
||||
const app = createServer({ config, authMiddleware, crawlingController, findingsController, ... });
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// 11. Socket.io
|
||||
const io = new Server(httpServer, { cors: { origin: config.cors.origin } });
|
||||
const gateway = new SocketGateway(io, eventBus);
|
||||
|
||||
// 12. Job queue
|
||||
const jobQueue = new SQLiteJobQueue(db, logger);
|
||||
jobQueue.start();
|
||||
|
||||
// 13. Listen
|
||||
httpServer.listen(config.port, config.host, () => {
|
||||
logger.info({ port: config.port }, 'ABE server ready');
|
||||
});
|
||||
|
||||
// 14. Graceful shutdown
|
||||
async function shutdown(signal: string) {
|
||||
logger.info({ signal }, 'Shutting down...');
|
||||
httpServer.close();
|
||||
io.close();
|
||||
jobQueue.pause();
|
||||
await jobQueue.waitForActive(30000);
|
||||
await db.destroy();
|
||||
logger.info('Shutdown complete');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
}
|
||||
|
||||
bootstrap().catch((err) => {
|
||||
console.error('Fatal: failed to start ABE', err);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## IMPORTANTE
|
||||
- El código existente en src/server/ debe DEJAR DE USARSE gradualmente
|
||||
- Mantener los endpoints viejos funcionando durante la migración
|
||||
- Cada controller es una clase con un `.router` getter que retorna Express.Router
|
||||
- NUNCA meter lógica de negocio en controllers — solo parse request → call use case → format response
|
||||
|
||||
66
.ralph/specs/phase-08-job-queue.md
Normal file
66
.ralph/specs/phase-08-job-queue.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Phase 8: Job Queue System
|
||||
|
||||
## Tabla jobs (SQLite)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
payload TEXT NOT NULL,
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
run_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_poll ON jobs(status, run_at, priority DESC);
|
||||
```
|
||||
|
||||
## Interface
|
||||
```typescript
|
||||
export interface IJobQueue {
|
||||
enqueue<T>(type: string, payload: T, opts?: { runAt?: Date; priority?: number; maxAttempts?: number }): Promise<string>;
|
||||
start(): void;
|
||||
pause(): void;
|
||||
waitForActive(timeoutMs: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Polling logic
|
||||
```
|
||||
loop (cada pollIntervalMs):
|
||||
SELECT id, type, payload FROM jobs
|
||||
WHERE status = 'pending' AND run_at <= datetime('now')
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
LIMIT 1
|
||||
|
||||
if found:
|
||||
UPDATE jobs SET status = 'running', started_at = now, attempts = attempts + 1
|
||||
WHERE id = ? AND status = 'pending' // optimistic lock
|
||||
|
||||
if updated 0 rows → skip (otro worker lo tomó)
|
||||
|
||||
try:
|
||||
result = await executeJob(type, payload)
|
||||
UPDATE jobs SET status = 'completed', result = ?, completed_at = now
|
||||
catch:
|
||||
if attempts >= max_attempts:
|
||||
UPDATE jobs SET status = 'failed', error = ?
|
||||
else:
|
||||
backoff = min(1000 * 2^attempts, 60000)
|
||||
UPDATE jobs SET status = 'pending', run_at = now + backoff, error = ?
|
||||
```
|
||||
|
||||
## Job types
|
||||
- `exploration:run` — payload: { sessionId, config }
|
||||
- `report:generate` — payload: { reportId, format, filters }
|
||||
- `cleanup:old-data` — payload: { retentionDays }
|
||||
|
||||
## NO usar Redis
|
||||
El job queue es SQLite-based para zero-dependency self-hosted.
|
||||
Es simple, funciona para el volumen esperado (decenas de jobs, no miles).
|
||||
148
.ralph/specs/phase-09-auth-module.md
Normal file
148
.ralph/specs/phase-09-auth-module.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Phase 9: Auth Module
|
||||
|
||||
## Objetivo
|
||||
Sistema completo de autenticación y autorización para ABE como plataforma.
|
||||
|
||||
## Roles y permisos
|
||||
|
||||
| Role | Sessions | Findings | Reports | Integrations | Org/Users | Settings | License |
|
||||
|---------|----------|----------|---------|--------------|-----------|----------|---------|
|
||||
| owner | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD |
|
||||
| admin | CRUD | CRUD | CRUD | CRUD | CRU | CRUD | R |
|
||||
| member | CR | CRU | CR | R | R | R | R |
|
||||
| viewer | R | R | R | R | R | R | R |
|
||||
|
||||
## Better Auth config
|
||||
```typescript
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
// Usar Kysely adapter o direct SQLite
|
||||
type: 'sqlite',
|
||||
url: config.db.path,
|
||||
},
|
||||
emailAndPassword: { enabled: true },
|
||||
session: {
|
||||
maxAge: config.auth.sessionMaxAge,
|
||||
updateAge: 60 * 60, // refresh cada hora
|
||||
},
|
||||
// Organization plugin si disponible, sino implementar manual
|
||||
});
|
||||
```
|
||||
|
||||
Si Better Auth no soporta organizaciones directamente, implementar manualmente:
|
||||
- Tabla organizations (id, name, slug, created_at)
|
||||
- Tabla org_members (id, org_id, user_id, role, invited_at, joined_at)
|
||||
|
||||
## CASL AbilityFactory
|
||||
```typescript
|
||||
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
|
||||
|
||||
export function defineAbilityFor(role: string) {
|
||||
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
|
||||
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
can('manage', 'all');
|
||||
break;
|
||||
case 'admin':
|
||||
can('manage', 'all');
|
||||
cannot('delete', 'Organization');
|
||||
cannot('manage', 'License');
|
||||
can('read', 'License');
|
||||
break;
|
||||
case 'member':
|
||||
can('create', ['Session', 'Finding', 'Report']);
|
||||
can('read', 'all');
|
||||
can('update', 'Finding');
|
||||
break;
|
||||
case 'viewer':
|
||||
can('read', 'all');
|
||||
break;
|
||||
}
|
||||
return build();
|
||||
}
|
||||
```
|
||||
|
||||
## AuthMiddleware — orden de verificación
|
||||
1. Check cookie de session (web UI) via Better Auth
|
||||
2. Check header `Authorization: Bearer <jwt>`
|
||||
3. Check header `X-ABE-API-Key: <key>` (API keys para CI/CD)
|
||||
4. Si ninguno → 401
|
||||
|
||||
## API Key system
|
||||
- POST /api/auth/api-keys — crear key (retorna key UNA vez, después solo hash)
|
||||
- GET /api/auth/api-keys — listar (sin mostrar key, solo nombre + último uso)
|
||||
- DELETE /api/auth/api-keys/:id — revocar
|
||||
- Keys hasheadas con SHA-256 en DB
|
||||
- Cada key tiene: name, permissions (array de roles), expiresAt, lastUsedAt
|
||||
|
||||
## First-run flow
|
||||
1. Backend: si tabla users tiene 0 rows → flag `setupRequired = true`
|
||||
2. GET /api/auth/setup-required → `{ required: boolean }`
|
||||
3. Si required, POST /api/auth/setup con { email, password, name, orgName }
|
||||
4. Crea user con role owner + organization default
|
||||
5. Después de setup, requiere login normal
|
||||
|
||||
## Migraciones
|
||||
```sql
|
||||
-- users (Better Auth maneja su propia tabla, pero añadir campos custom)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS org_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
org_id TEXT NOT NULL REFERENCES organizations(id),
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
joined_at INTEGER NOT NULL,
|
||||
UNIQUE(org_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
org_id TEXT NOT NULL REFERENCES organizations(id),
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL, -- primeros 8 chars para identificar
|
||||
permissions TEXT NOT NULL DEFAULT '["member"]',
|
||||
expires_at INTEGER,
|
||||
last_used_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## NOTA sobre Better Auth
|
||||
Si Better Auth resulta demasiado complejo de integrar con Express puro
|
||||
o tiene incompatibilidades, implementar auth manualmente:
|
||||
- argon2 para hash passwords
|
||||
- crypto.randomUUID() para session tokens
|
||||
- Cookie httpOnly + secure + sameSite para sessions
|
||||
- Middleware custom que lee cookie → busca en auth_sessions → adjunta user a req
|
||||
|
||||
Esto es PERFECTAMENTE VÁLIDO. No over-engineer la auth.
|
||||
La prioridad es que funcione, sea seguro, y tenga RBAC.
|
||||
129
.ralph/specs/phase-10-frontend-shell.md
Normal file
129
.ralph/specs/phase-10-frontend-shell.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Phase 10: Frontend Shell — shadcn/ui
|
||||
|
||||
## Setup shadcn/ui
|
||||
```bash
|
||||
cd frontend
|
||||
npx shadcn@latest init
|
||||
# Responder: Vite, Zinc, CSS variables, YES to tailwind
|
||||
```
|
||||
|
||||
## Layout principal
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ TopBar: [☰] [ABE logo] ···· [⌘K Search] [🌙] [👤]│
|
||||
├────────┬───────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Side- │ Content Area │
|
||||
│ bar │ (React Router Outlet) │
|
||||
│ │ │
|
||||
│ 📊 Dashboard │
|
||||
│ 🔍 Explorations │
|
||||
│ 🐛 Findings │
|
||||
│ 📄 Reports │
|
||||
│ ───────── │
|
||||
│ ⚙️ Settings │
|
||||
│ │ │
|
||||
└────────┴───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Dark mode (DEFAULT)
|
||||
- Usar estrategia class-based: `<html class="dark">`
|
||||
- CSS variables de shadcn ya soportan dark mode
|
||||
- Toggle en TopBar: sol/luna
|
||||
- Persistir en localStorage key 'abe-theme'
|
||||
|
||||
## Auth flow en frontend
|
||||
```
|
||||
App monta →
|
||||
GET /api/auth/setup-required
|
||||
→ si required: mostrar /setup
|
||||
→ si no:
|
||||
GET /api/auth/me
|
||||
→ si 401: redirect /login
|
||||
→ si ok: render AppLayout con user data
|
||||
```
|
||||
|
||||
## API client (lib/api.ts)
|
||||
```typescript
|
||||
const API_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_URL}${path}`, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
```
|
||||
|
||||
## Routing
|
||||
```typescript
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/setup" element={<Setup />} />
|
||||
<Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/sessions" element={<SessionList />} />
|
||||
<Route path="/sessions/:id" element={<SessionDetail />} />
|
||||
<Route path="/findings" element={<FindingsList />} />
|
||||
<Route path="/findings/:id" element={<FindingDetail />} />
|
||||
<Route path="/reports" element={<Reports />} />
|
||||
<Route path="/visual-review" element={<VisualReview />} />
|
||||
<Route path="/settings/*" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
## Command Palette (⌘K)
|
||||
Powered by shadcn Command (cmdk):
|
||||
- Buscar por: sessions, findings, settings sections
|
||||
- Acciones: "New Exploration", "Generate Report"
|
||||
- Keyboard: ⌘K abre, Esc cierra
|
||||
|
||||
## File structure
|
||||
```
|
||||
frontend/src/
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn generados (NO tocar)
|
||||
│ ├── layout/
|
||||
│ │ ├── AppLayout.tsx
|
||||
│ │ ├── AppSidebar.tsx
|
||||
│ │ ├── TopBar.tsx
|
||||
│ │ ├── CommandPalette.tsx
|
||||
│ │ ├── ProtectedRoute.tsx
|
||||
│ │ └── ThemeProvider.tsx
|
||||
│ └── common/
|
||||
│ └── SeverityBadge.tsx
|
||||
├── hooks/
|
||||
│ ├── useAuth.ts
|
||||
│ └── useSocket.ts
|
||||
├── lib/
|
||||
│ ├── api.ts
|
||||
│ ├── queryClient.ts
|
||||
│ └── utils.ts # cn() de shadcn
|
||||
├── stores/
|
||||
│ └── uiStore.ts
|
||||
├── pages/
|
||||
│ ├── Dashboard.tsx (placeholder "Coming in Phase 11")
|
||||
│ ├── Login.tsx
|
||||
│ └── Setup.tsx
|
||||
├── App.tsx
|
||||
└── main.tsx
|
||||
```
|
||||
|
||||
## IMPORTANTE
|
||||
- El Dashboard en esta fase puede ser un placeholder que diga "Dashboard — Coming soon"
|
||||
- Lo importante es que el shell funcione: login → sidebar → routing → theme
|
||||
- NO intentar hacer todo el dashboard aquí — eso es Phase 11
|
||||
Reference in New Issue
Block a user