Files
echo-core/tools/ralph_prd_generator.py
MoltBot Service 5928077646 cleanup: remove clawd/openclaw references, fix permissions, add architecture docs
- Replace all ~/clawd and ~/.clawdbot paths with ~/echo-core equivalents
  in tools (git_commit, ralph_prd_generator, backup_config, lead-gen)
- Update personality files: TOOLS.md repo/paths, AGENTS.md security audit cmd
- Migrate HANDOFF.md architectural decisions to docs/architecture.md
- Tighten credentials/ dir to 700, add to .gitignore
- Add .claude/ and *.pid to .gitignore
- Various adapter, router, and session improvements from prior work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:44:13 +00:00

489 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Ralph PRD Generator - implementare Python a skill-ului ralph-prd
Generează PRD și prd.json fără să apeleze Claude Code
"""
import json
import re
from pathlib import Path
from datetime import datetime
def detect_project_context(project_dir: Path):
"""Detectează tech stack și mod (NEW_PROJECT vs FEATURE)"""
context = {
"mode": "NEW_PROJECT",
"stack_type": None,
"config_file": None,
"scripts": {},
"dependencies": []
}
# Verifică fișiere de config
config_files = {
"package.json": "nodejs",
"pyproject.toml": "python",
"requirements.txt": "python-legacy",
"go.mod": "go",
"Cargo.toml": "rust",
"pom.xml": "java-maven",
"build.gradle": "java-gradle",
"composer.json": "php",
"Gemfile": "ruby"
}
for filename, stack in config_files.items():
config_path = project_dir / filename
if config_path.exists():
context["mode"] = "FEATURE"
context["stack_type"] = stack
context["config_file"] = filename
# Extrage info din package.json
if filename == "package.json":
try:
with open(config_path) as f:
data = json.load(f)
context["scripts"] = data.get("scripts", {})
context["dependencies"] = list(data.get("dependencies", {}).keys())
except:
pass
break
return context
def generate_prd_markdown(project_name: str, description: str, context: dict):
"""Generează PRD markdown bazat pe descriere și context"""
# Parse descriere pentru a extrage info
lines = description.strip().split('\n')
features = []
requirements = []
for line in lines:
line = line.strip()
if line.startswith('Features:') or line.startswith('Feature:'):
continue
if line.startswith('-'):
features.append(line[1:].strip())
elif line and not line.endswith(':'):
requirements.append(line)
if not features:
# Dacă nu sunt features explicit, folosim descrierea ca feature
features = [description.split('.')[0]]
# Detectează tip proiect din descriere
desc_lower = description.lower()
if 'cli' in desc_lower or 'command' in desc_lower:
project_type = "CLI Tool"
elif 'api' in desc_lower or 'backend' in desc_lower:
project_type = "API / Backend"
elif 'web' in desc_lower or 'app' in desc_lower:
project_type = "Web Application"
else:
project_type = "Application"
# Detectează stack din descriere sau context
if 'python' in desc_lower:
stack = "Python"
test_framework = "pytest"
elif 'javascript' in desc_lower or 'node' in desc_lower or 'typescript' in desc_lower:
stack = "Node.js / TypeScript"
test_framework = "jest"
elif context["stack_type"] == "nodejs":
stack = "Node.js / TypeScript"
test_framework = "jest"
elif context["stack_type"] and context["stack_type"].startswith("python"):
stack = "Python"
test_framework = "pytest"
else:
stack = "Python" # Default
test_framework = "pytest"
# Template PRD
prd = f"""# PRD: {project_name.replace('-', ' ').title()}
## 1. Introducere
{description.split('.')[0]}.
**Data:** {datetime.now().strftime('%Y-%m-%d')}
**Status:** Draft
**Mode:** {context['mode']}
## 2. Context Tehnic
"""
if context["mode"] == "FEATURE":
prd += f"""**Proiect existent detectat:**
- Stack: {context['stack_type']}
- Config: {context['config_file']}
- Scripts: {', '.join(context['scripts'].keys())}
"""
else:
prd += f"""**Proiect nou:**
- Tip: {project_type}
- Stack recomandat: {stack}
- Test framework: {test_framework}
"""
prd += f"""## 3. Obiective
### Obiectiv Principal
{features[0] if features else description.split('.')[0]}
### Obiective Secundare
"""
for i, feature in enumerate(features[1:] if len(features) > 1 else features, 1):
prd += f"- {feature}\n"
prd += f"""
### Metrici de Succes
- Toate funcționalitățile implementate conform spec
- Tests passing (coverage > 80%)
- Code quality: lint + typecheck pass
## 4. User Stories
"""
# Generează user stories din features
for i, feature in enumerate(features, 1):
story_id = f"US-{i:03d}"
title = feature.strip()
prd += f"""### {story_id}: {title}
**Ca** utilizator
**Vreau** {title.lower()}
**Pentru că** pot folosi aplicația eficient
**Acceptance Criteria:**
- [ ] Funcționalitatea implementată conform descrierii
- [ ] Input validation în loc
- [ ] Error handling pentru cazuri edge
- [ ] Tests cu {test_framework} (coverage > 80%)
- [ ] Code quality: lint + typecheck pass
**Priority:** {i * 10}
"""
# Dacă nu sunt multe features, adaugă story pentru tests
if len(features) < 3:
prd += f"""### US-{len(features)+1:03d}: Tests și Documentație
**Ca** developer
**Vreau** teste comprehensive și documentație
**Pentru că** asigur calitatea codului
**Acceptance Criteria:**
- [ ] Unit tests pentru toate funcțiile (coverage > 80%)
- [ ] Integration tests pentru flow-uri principale
- [ ] README cu instrucțiuni de utilizare
- [ ] Docstrings pentru funcții publice
- [ ] {test_framework} rulează fără erori
**Priority:** {(len(features)+1) * 10}
"""
prd += f"""## 5. Cerințe Funcționale
"""
for i, req in enumerate(requirements if requirements else features, 1):
prd += f"{i}. [REQ-{i:03d}] {req}\n"
prd += f"""
## 6. Non-Goals (Ce NU facem)
- Interfață grafică (GUI) - doar CLI/API
- Suport multiple limbaje - doar {stack}
- Deployment infrastructure - doar cod functional
## 7. Considerații Tehnice
### Stack/Tehnologii
- Limbaj: {stack}
- Testing: {test_framework}
- Linting: pylint / eslint (depinde de stack)
- Type checking: mypy / typescript
### Patterns de Urmat
- Clean code principles
- SOLID principles unde aplicabil
- Error handling consistent
- Input validation strict
### Riscuri Tehnice
- Edge cases la input validation
- Performance pentru volume mari de date (dacă aplicabil)
## 8. Considerații Security
- Input validation pentru toate datele externe
- Error messages fără info sensibilă
- Principle of least privilege
## 9. Open Questions
- [ ] Performance requirements specifice?
- [ ] Limite pe input sizes?
- [ ] Specific error handling patterns preferați?
---
**Generated by:** Echo (Ralph PRD Generator)
**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
"""
return prd
def prd_to_stories(prd_content: str, project_name: str):
"""Extrage user stories din PRD și le convertește în format prd.json"""
stories = []
# Parse PRD pentru stories
story_pattern = r'### (US-\d+): (.+?)\n\*\*Ca\*\* (.+?)\n\*\*Vreau\*\* (.+?)\n\*\*Pentru că\*\* (.+?)\n\n\*\*Acceptance Criteria:\*\*\n(.*?)\n\n\*\*Priority:\*\* (\d+)'
matches = re.finditer(story_pattern, prd_content, re.DOTALL)
for match in matches:
story_id = match.group(1)
title = match.group(2).strip()
user_type = match.group(3).strip()
want = match.group(4).strip()
because = match.group(5).strip()
criteria_text = match.group(6).strip()
priority = int(match.group(7))
# Parse acceptance criteria
criteria = []
for line in criteria_text.split('\n'):
line = line.strip()
if line.startswith('- [ ]'):
criteria.append(line[5:].strip())
# Detectează dacă necesită browser check (pentru UI)
requires_browser = 'ui' in title.lower() or 'interface' in title.lower()
story = {
"id": story_id,
"title": title,
"description": f"Ca {user_type}, vreau {want} pentru că {because}",
"priority": priority,
"acceptanceCriteria": criteria,
"requiresBrowserCheck": requires_browser,
"passes": False,
"notes": ""
}
stories.append(story)
# Dacă nu găsim stories (regex failed), generăm basic
if not stories:
stories = [{
"id": "US-001",
"title": "Implementare funcționalitate principală",
"description": f"Implementează {project_name}",
"priority": 10,
"acceptanceCriteria": [
"Funcționalitatea implementată",
"Tests passing",
"Lint + typecheck pass"
],
"requiresBrowserCheck": False,
"passes": False,
"notes": ""
}]
return stories
def detect_tech_stack_commands(project_dir: Path, context: dict):
"""Detectează comenzile tech stack pentru prd.json"""
stack_type = context.get("stack_type", "python")
scripts = context.get("scripts", {})
# Default commands per stack
if stack_type == "nodejs" or "package.json" in str(context.get("config_file", "")):
commands = {
"start": scripts.get("dev", scripts.get("start", "npm run dev")),
"build": scripts.get("build", "npm run build"),
"lint": scripts.get("lint", "npm run lint"),
"typecheck": scripts.get("typecheck", "npm run typecheck"),
"test": scripts.get("test", "npm test")
}
port = 3000
stack_name = "nextjs" if "next" in str(context.get("dependencies", [])) else "nodejs"
elif stack_type and stack_type.startswith("python"):
commands = {
"start": "python main.py",
"build": "",
"lint": "ruff check .",
"typecheck": "mypy .",
"test": "pytest"
}
port = 8000
stack_name = "python"
else:
# Generic/fallback
commands = {
"start": "python main.py",
"build": "",
"lint": "echo 'No linter configured'",
"typecheck": "echo 'No typecheck configured'",
"test": "pytest"
}
port = 8000
stack_name = "python"
return {
"type": stack_name,
"commands": commands,
"port": port
}
def create_prd_and_json(project_name: str, description: str, workspace_dir: Path):
"""
Generează PRD markdown și prd.json pentru un proiect
Returns:
tuple: (prd_file_path, prd_json_path) sau (None, None) dacă eroare
"""
project_dir = workspace_dir / project_name
project_dir.mkdir(parents=True, exist_ok=True)
# Detectează context
context = detect_project_context(project_dir)
print(f"📊 Context detectat:")
print(f" Mode: {context['mode']}")
if context['stack_type']:
print(f" Stack: {context['stack_type']}")
print(f" Config: {context['config_file']}")
# Generează PRD markdown
prd_content = generate_prd_markdown(project_name, description, context)
# Salvează PRD
tasks_dir = project_dir / "tasks"
tasks_dir.mkdir(exist_ok=True)
prd_file = tasks_dir / f"prd-{project_name}.md"
with open(prd_file, 'w', encoding='utf-8') as f:
f.write(prd_content)
print(f"✅ PRD salvat: {prd_file}")
# Generează prd.json
stories = prd_to_stories(prd_content, project_name)
tech_stack = detect_tech_stack_commands(project_dir, context)
prd_json_data = {
"projectName": project_name,
"branchName": f"ralph/{project_name}",
"description": description.split('\n')[0],
"techStack": tech_stack,
"userStories": stories
}
# Creează structura ralph
ralph_dir = project_dir / "scripts" / "ralph"
ralph_dir.mkdir(parents=True, exist_ok=True)
(ralph_dir / "logs").mkdir(exist_ok=True)
(ralph_dir / "archive").mkdir(exist_ok=True)
(ralph_dir / "screenshots").mkdir(exist_ok=True)
# Salvează prd.json
prd_json_file = ralph_dir / "prd.json"
with open(prd_json_file, 'w', encoding='utf-8') as f:
json.dump(prd_json_data, f, indent=2, ensure_ascii=False)
print(f"✅ prd.json salvat: {prd_json_file}")
print(f"📋 Stories: {len(stories)}")
for story in stories:
print(f" - {story['id']}: {story['title']}")
# Copiază template-uri ralph
templates_dir = Path.home() / ".claude" / "skills" / "ralph" / "templates"
if not templates_dir.exists():
templates_dir = Path.home() / "echo-core" / "skills" / "ralph" / "templates"
if templates_dir.exists():
# Copiază ralph.sh
ralph_sh_src = templates_dir / "ralph.sh"
if ralph_sh_src.exists():
ralph_sh_dst = ralph_dir / "ralph.sh"
with open(ralph_sh_src) as f:
content = f.read()
with open(ralph_sh_dst, 'w') as f:
f.write(content)
ralph_sh_dst.chmod(0o755)
print(f"✅ ralph.sh copiat")
# Copiază prompt.md
prompt_src = templates_dir / "prompt.md"
if prompt_src.exists():
prompt_dst = ralph_dir / "prompt.md"
with open(prompt_src) as f:
content = f.read()
with open(prompt_dst, 'w') as f:
f.write(content)
print(f"✅ prompt.md copiat")
# Init progress.txt
progress_file = ralph_dir / "progress.txt"
with open(progress_file, 'w') as f:
f.write(f"# Ralph Progress Log\n")
f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
f.write(f"Project: {project_name}\n")
f.write(f"---\n")
print(f"✅ Structură Ralph completă în {ralph_dir}")
return prd_file, prd_json_file
if __name__ == "__main__":
import sys
if len(sys.argv) < 3:
print("Usage: python ralph_prd_generator.py PROJECT_NAME 'description'")
sys.exit(1)
project_name = sys.argv[1]
description = sys.argv[2]
workspace = Path.home() / "workspace"
print(f"🔄 Generez PRD pentru {project_name}")
print("=" * 70)
prd_file, prd_json = create_prd_and_json(project_name, description, workspace)
if prd_file and prd_json:
print("\n" + "=" * 70)
print("✅ PRD și prd.json generate cu succes!")
print(f"📄 PRD: {prd_file}")
print(f"📋 JSON: {prd_json}")
print("\n📌 Următorii pași:")
print(f" 1. Revizuiește PRD în {prd_file}")
print(f" 2. Rulează Ralph: cd {prd_file.parent.parent} && ./scripts/ralph/ralph.sh 20")
print("=" * 70)
else:
print("\n❌ Eroare la generare PRD")
sys.exit(1)