Merge pull request #369 from alirezarezvani/feat/code-to-prd
feat(product-team): add code-to-prd skill — reverse-engineer any codebase into PRD
This commit is contained in:
@@ -394,26 +394,31 @@
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "agenthub",
|
||||
"source": "./engineering/agenthub",
|
||||
"description": "Multi-agent collaboration — spawn N parallel subagents that compete on code optimization, content drafts, research approaches, or any task that benefits from diverse solutions. 7 slash commands (/hub:init, /hub:spawn, /hub:status, /hub:eval, /hub:merge, /hub:board, /hub:run), agent templates, DAG-based orchestration, LLM judge mode, message board coordination.",
|
||||
"name": "code-to-prd",
|
||||
"source": "./product-team/code-to-prd",
|
||||
"description": "Reverse-engineer any codebase into a complete PRD. Frontend (React, Vue, Angular, Next.js), backend (NestJS, Django, Express, FastAPI), and fullstack. 2 Python scripts (codebase_analyzer, prd_scaffolder), 2 reference guides, /code-to-prd slash command.",
|
||||
"version": "2.1.2",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani"
|
||||
},
|
||||
"keywords": [
|
||||
"multi-agent",
|
||||
"collaboration",
|
||||
"parallel",
|
||||
"git-dag",
|
||||
"orchestration",
|
||||
"competition",
|
||||
"worktree",
|
||||
"content-generation",
|
||||
"research",
|
||||
"optimization"
|
||||
"prd",
|
||||
"product-requirements",
|
||||
"reverse-engineering",
|
||||
"frontend",
|
||||
"backend",
|
||||
"fullstack",
|
||||
"documentation",
|
||||
"code-analysis",
|
||||
"react",
|
||||
"vue",
|
||||
"angular",
|
||||
"nestjs",
|
||||
"django",
|
||||
"fastapi",
|
||||
"express"
|
||||
],
|
||||
"category": "development"
|
||||
"category": "product"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
78
commands/code-to-prd.md
Normal file
78
commands/code-to-prd.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: code-to-prd
|
||||
description: Reverse-engineer a frontend codebase into a PRD. Usage: /code-to-prd [path]
|
||||
---
|
||||
|
||||
# /code-to-prd
|
||||
|
||||
Reverse-engineer a frontend codebase into a complete Product Requirements Document.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/code-to-prd # Analyze current project
|
||||
/code-to-prd ./src # Analyze specific directory
|
||||
/code-to-prd /path/to/project # Analyze external project
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Scan** — Run `codebase_analyzer.py` to detect framework, routes, APIs, enums, and project structure
|
||||
2. **Scaffold** — Run `prd_scaffolder.py` to create `prd/` directory with README.md, per-page stubs, and appendix files
|
||||
3. **Analyze** — Walk through each page following the Phase 2 workflow: fields, interactions, API dependencies, page relationships
|
||||
4. **Generate** — Produce the final PRD with all pages, enum dictionary, API inventory, and page relationship map
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Analyze
|
||||
|
||||
Determine the project path (default: current directory). Run the frontend analyzer:
|
||||
|
||||
```bash
|
||||
python3 {skill_path}/scripts/codebase_analyzer.py {project_path} -o .code-to-prd-analysis.json
|
||||
```
|
||||
|
||||
Display a summary of findings: framework, page count, API count, enum count.
|
||||
|
||||
### Step 2: Scaffold
|
||||
|
||||
Generate the PRD directory skeleton:
|
||||
|
||||
```bash
|
||||
python3 {skill_path}/scripts/prd_scaffolder.py .code-to-prd-analysis.json -o prd/
|
||||
```
|
||||
|
||||
### Step 3: Fill
|
||||
|
||||
For each page in the inventory, follow the SKILL.md Phase 2 workflow:
|
||||
- Read the page's component files
|
||||
- Document fields, interactions, API dependencies, page relationships
|
||||
- Fill in the corresponding `prd/pages/` stub
|
||||
|
||||
Work in batches of 3-5 pages for large projects (>15 pages). Ask the user to confirm after each batch.
|
||||
|
||||
### Step 4: Finalize
|
||||
|
||||
Complete the appendix files:
|
||||
- `prd/appendix/enum-dictionary.md` — all enums and status codes found
|
||||
- `prd/appendix/api-inventory.md` — consolidated API reference
|
||||
- `prd/appendix/page-relationships.md` — navigation and data coupling map
|
||||
|
||||
Clean up the temporary analysis file:
|
||||
```bash
|
||||
rm .code-to-prd-analysis.json
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
A `prd/` directory containing:
|
||||
- `README.md` — system overview, module map, page inventory
|
||||
- `pages/*.md` — one file per page with fields, interactions, APIs
|
||||
- `appendix/*.md` — enum dictionary, API inventory, page relationships
|
||||
|
||||
## Skill Reference
|
||||
|
||||
- `product-team/code-to-prd/SKILL.md`
|
||||
- `product-team/code-to-prd/scripts/codebase_analyzer.py`
|
||||
- `product-team/code-to-prd/scripts/prd_scaffolder.py`
|
||||
- `product-team/code-to-prd/references/prd-quality-checklist.md`
|
||||
13
product-team/code-to-prd/.claude-plugin/plugin.json
Normal file
13
product-team/code-to-prd/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "code-to-prd",
|
||||
"description": "Reverse-engineer any codebase into a complete Product Requirements Document (PRD). Analyzes routes, components, models, APIs, and interactions for frontend (React, Vue, Angular, Next.js), backend (NestJS, Django, Express, FastAPI), and fullstack applications.",
|
||||
"version": "2.1.2",
|
||||
"author": {
|
||||
"name": "Alireza Rezvani",
|
||||
"url": "https://alirezarezvani.com"
|
||||
},
|
||||
"homepage": "https://github.com/alirezarezvani/claude-skills/tree/main/product-team/code-to-prd",
|
||||
"repository": "https://github.com/alirezarezvani/claude-skills",
|
||||
"license": "MIT",
|
||||
"skills": "./"
|
||||
}
|
||||
58
product-team/code-to-prd/README.md
Normal file
58
product-team/code-to-prd/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Code → PRD
|
||||
|
||||
Reverse-engineer any codebase into a complete Product Requirements Document (PRD).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# One command
|
||||
/code-to-prd /path/to/project
|
||||
|
||||
# Or step by step
|
||||
python3 scripts/codebase_analyzer.py /path/to/project -o analysis.json
|
||||
python3 scripts/prd_scaffolder.py analysis.json -o prd/ -n "My App"
|
||||
```
|
||||
|
||||
## Supported Frameworks
|
||||
|
||||
| Stack | Frameworks |
|
||||
|-------|-----------|
|
||||
| Frontend | React, Vue, Angular, Svelte, Next.js, Nuxt, SvelteKit, Remix |
|
||||
| Backend | NestJS, Express, Django, DRF, FastAPI, Flask |
|
||||
| Fullstack | Next.js (pages + API), Nuxt (pages + server), Django (views + templates) |
|
||||
|
||||
## What It Generates
|
||||
|
||||
```
|
||||
prd/
|
||||
├── README.md # System overview
|
||||
├── pages/
|
||||
│ ├── 01-user-mgmt-list.md # Per-page/endpoint docs
|
||||
│ └── ...
|
||||
└── appendix/
|
||||
├── enum-dictionary.md # All enums and status codes
|
||||
├── api-inventory.md # Complete API reference
|
||||
└── page-relationships.md # Navigation and data coupling
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `codebase_analyzer.py` | Scan codebase → extract routes, APIs, models, enums |
|
||||
| `prd_scaffolder.py` | Generate PRD directory skeleton from analysis JSON |
|
||||
|
||||
Both are stdlib-only — no pip install needed. Run `--help` for full usage.
|
||||
|
||||
## References
|
||||
|
||||
- `references/framework-patterns.md` — Route, state, API, form, and model patterns per framework
|
||||
- `references/prd-quality-checklist.md` — Validation checklist for completeness and accuracy
|
||||
|
||||
## Attribution
|
||||
|
||||
Inspired by [code-to-prd](https://github.com/lihanglogan/code-to-prd) by [@lihanglogan](https://github.com/lihanglogan).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
507
product-team/code-to-prd/SKILL.md
Normal file
507
product-team/code-to-prd/SKILL.md
Normal file
@@ -0,0 +1,507 @@
|
||||
---
|
||||
Name: code-to-prd
|
||||
Tier: STANDARD
|
||||
Category: product
|
||||
Dependencies: none
|
||||
Author: Alireza Rezvani
|
||||
Version: 2.1.2
|
||||
name: code-to-prd
|
||||
description: |
|
||||
Reverse-engineer any codebase into a complete Product Requirements Document (PRD).
|
||||
Analyzes routes, components, state management, API integrations, and user interactions to produce
|
||||
business-readable documentation detailed enough for engineers or AI agents to fully reconstruct
|
||||
every page and endpoint. Works with frontend frameworks (React, Vue, Angular, Svelte, Next.js, Nuxt),
|
||||
backend frameworks (NestJS, Django, Express, FastAPI), and fullstack applications.
|
||||
|
||||
Trigger when users mention: generate PRD, reverse-engineer requirements, code to documentation,
|
||||
extract product specs from code, document page logic, analyze page fields and interactions,
|
||||
create a functional inventory, write requirements from an existing codebase, document API endpoints,
|
||||
or analyze backend routes.
|
||||
license: MIT
|
||||
metadata:
|
||||
updated: 2026-03-17
|
||||
---
|
||||
|
||||
## Name
|
||||
|
||||
Code → PRD
|
||||
|
||||
## Description
|
||||
|
||||
Reverse-engineer any frontend, backend, or fullstack codebase into a complete Product Requirements Document (PRD). Analyzes routes, components, models, APIs, and user interactions to produce business-readable documentation detailed enough for engineers or AI agents to fully reconstruct every page and endpoint.
|
||||
|
||||
# Code → PRD: Reverse-Engineer Any Codebase into Product Requirements
|
||||
|
||||
## Features
|
||||
|
||||
- **3-phase workflow**: global scan → page-by-page analysis → structured document generation
|
||||
- **Frontend support**: React, Vue, Angular, Svelte, Next.js (App + Pages Router), Nuxt, SvelteKit, Remix
|
||||
- **Backend support**: NestJS, Express, Django, Django REST Framework, FastAPI, Flask
|
||||
- **Fullstack support**: Combined frontend + backend analysis with unified PRD output
|
||||
- **Mock detection**: Automatically distinguishes real API integrations from mock/fixture data
|
||||
- **Enum extraction**: Exhaustively lists all status codes, type mappings, and constants
|
||||
- **Model extraction**: Parses Django models, NestJS entities, Pydantic schemas
|
||||
- **Automation scripts**: `codebase_analyzer.py` for scanning, `prd_scaffolder.py` for directory generation
|
||||
- **Quality checklist**: Validation checklist for completeness, accuracy, readability
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Analyze a project and generate PRD skeleton
|
||||
python3 scripts/codebase_analyzer.py /path/to/project -o analysis.json
|
||||
python3 scripts/prd_scaffolder.py analysis.json -o prd/ -n "My App"
|
||||
|
||||
# Or use the slash command
|
||||
/code-to-prd /path/to/project
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Frontend (React)
|
||||
```bash
|
||||
/code-to-prd ./src
|
||||
# → Scans components, routes, API calls, state management
|
||||
# → Generates prd/ with per-page docs, enum dictionary, API inventory
|
||||
```
|
||||
|
||||
### Backend (Django)
|
||||
```bash
|
||||
/code-to-prd ./myproject
|
||||
# → Detects Django via manage.py, scans urls.py, views.py, models.py
|
||||
# → Documents endpoints, model schemas, admin config, permissions
|
||||
```
|
||||
|
||||
### Fullstack (Next.js)
|
||||
```bash
|
||||
/code-to-prd .
|
||||
# → Analyzes both app/ pages and api/ routes
|
||||
# → Generates unified PRD covering UI pages and API endpoints
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a senior product analyst and technical architect. Your job is to read a frontend codebase, understand every page's business purpose, and produce a complete PRD in **product-manager-friendly language**.
|
||||
|
||||
### Dual Audience
|
||||
|
||||
1. **Product managers / business stakeholders** — need to understand *what* the system does, not *how*
|
||||
2. **Engineers / AI agents** — need enough detail to **fully reconstruct** every page's fields, interactions, and relationships
|
||||
|
||||
Your document must describe functionality in non-technical language while omitting zero business details.
|
||||
|
||||
### Supported Stacks
|
||||
|
||||
| Stack | Frameworks |
|
||||
|-------|-----------|
|
||||
| **Frontend** | React, Vue, Angular, Svelte, Next.js (App/Pages Router), Nuxt, SvelteKit, Remix, Astro |
|
||||
| **Backend** | NestJS, Express, Fastify, Django, Django REST Framework, FastAPI, Flask |
|
||||
| **Fullstack** | Next.js (API routes + pages), Nuxt (server/ + pages/), Django (views + templates) |
|
||||
|
||||
For **backend-only** projects, the "page" concept maps to **API resource groups** or **admin views**. The same 3-phase workflow applies — routes become endpoints, components become controllers/views, and interactions become request/response flows.
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1 — Project Global Scan
|
||||
|
||||
Build global context before diving into pages.
|
||||
|
||||
#### 1. Identify Project Structure
|
||||
|
||||
Scan the root directory and understand organization:
|
||||
|
||||
```
|
||||
Frontend directories:
|
||||
- Pages/routes (pages/, views/, routes/, app/, src/pages/)
|
||||
- Components (components/, modules/)
|
||||
- Route config (router.ts, routes.ts, App.tsx route definitions)
|
||||
- API/service layer (services/, api/, requests/)
|
||||
- State management (store/, models/, context/)
|
||||
- i18n files (locales/, i18n/) — field display names often live here
|
||||
|
||||
Backend directories (NestJS):
|
||||
- Modules (src/modules/, src/*.module.ts)
|
||||
- Controllers (*.controller.ts) — route handlers
|
||||
- Services (*.service.ts) — business logic
|
||||
- DTOs (dto/, *.dto.ts) — request/response shapes
|
||||
- Entities (entities/, *.entity.ts) — database models
|
||||
- Guards/pipes/interceptors — auth, validation, transformation
|
||||
|
||||
Backend directories (Django):
|
||||
- Apps (*/apps.py, */views.py, */models.py, */urls.py)
|
||||
- URL config (urls.py, */urls.py)
|
||||
- Views (views.py, viewsets.py) — route handlers
|
||||
- Models (models.py) — database schema
|
||||
- Serializers (serializers.py) — request/response shapes
|
||||
- Forms (forms.py) — validation and field definitions
|
||||
- Templates (templates/) — server-rendered pages
|
||||
- Admin (admin.py) — admin panel configuration
|
||||
```
|
||||
|
||||
**Identify framework** from `package.json` (Node.js frameworks) or project files (`manage.py` for Django, `requirements.txt`/`pyproject.toml` for Python). Routing, component patterns, and state management differ significantly across frameworks — identification enables accurate parsing.
|
||||
|
||||
#### 2. Build Route & Page Inventory
|
||||
|
||||
Extract all pages from route config into a complete **page inventory**:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| Route path | e.g. `/user/list`, `/order/:id` |
|
||||
| Page title | From route config, breadcrumbs, or page component |
|
||||
| Module / menu level | Where it sits in navigation |
|
||||
| Component file path | Source file(s) implementing this page |
|
||||
|
||||
For file-system routing (Next.js, Nuxt), infer from directory structure.
|
||||
|
||||
**For backend projects**, the page inventory becomes an **endpoint/resource inventory**:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| Endpoint path | e.g. `/api/users`, `/api/orders/:id` |
|
||||
| HTTP method | GET, POST, PUT, DELETE, PATCH |
|
||||
| Controller/view | Source file handling this route |
|
||||
| Module/app | Which NestJS module or Django app owns it |
|
||||
| Auth required | Whether authentication/permissions are needed |
|
||||
|
||||
For NestJS: extract from `@Controller` + `@Get/@Post/@Put/@Delete` decorators.
|
||||
For Django: extract from `urls.py` → `urlpatterns` and `viewsets.py` → router registrations.
|
||||
|
||||
#### 3. Map Global Context
|
||||
|
||||
Before analyzing individual pages, capture:
|
||||
|
||||
- **Global state** — user info, permissions, feature flags, config
|
||||
- **Shared components** — layout, nav, auth guards, error boundaries
|
||||
- **Enums & constants** — status codes, type mappings, role definitions
|
||||
- **API base config** — base URL, interceptors, auth headers, error handling
|
||||
- **Database models** (backend) — entity relationships, field types, constraints
|
||||
- **Middleware** (backend) — auth middleware, rate limiting, logging, CORS
|
||||
- **DTOs/Serializers** (backend) — request validation shapes, response formats
|
||||
|
||||
These will be referenced throughout page/endpoint analysis.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Page-by-Page Deep Analysis
|
||||
|
||||
Analyze every page in the inventory. **Each page produces its own Markdown file.**
|
||||
|
||||
#### Analysis Dimensions
|
||||
|
||||
For each page, answer:
|
||||
|
||||
##### A. Page Overview
|
||||
- What does this page do? (one sentence)
|
||||
- Where does it fit in the system?
|
||||
- What scenario brings a user here?
|
||||
|
||||
##### B. Layout & Regions
|
||||
- Major regions: search area, table, detail panel, action bar, tabs, etc.
|
||||
- Spatial arrangement: top/bottom, left/right, nested
|
||||
|
||||
##### C. Field Inventory (core — be exhaustive)
|
||||
|
||||
**For form pages**, list every field:
|
||||
|
||||
| Field Name | Type | Required | Default | Validation | Business Description |
|
||||
|-----------|------|----------|---------|------------|---------------------|
|
||||
| Username | Text input | Yes | — | Max 20 chars | System login account |
|
||||
|
||||
**For table/list pages**, list:
|
||||
- Search/filter fields (type, required, enum options)
|
||||
- Table columns (name, format, sortable, filterable)
|
||||
- Row action buttons (what each one does)
|
||||
|
||||
**Field name extraction priority:**
|
||||
1. Hardcoded display text in code
|
||||
2. i18n translation values
|
||||
3. Component `placeholder` / `label` / `title` props
|
||||
4. Variable names (last resort — provide reasonable display name)
|
||||
|
||||
##### D. Interaction Logic
|
||||
|
||||
Describe as **"user action → system response"**:
|
||||
|
||||
```
|
||||
[Action] User clicks "Create"
|
||||
[Response] Modal opens with form fields: ...
|
||||
[Validation] Name required, phone format check
|
||||
[API] POST /api/user/create with form data
|
||||
[Success] Toast "Created successfully", close modal, refresh list
|
||||
[Failure] Show API error message
|
||||
```
|
||||
|
||||
**Cover all interaction types:**
|
||||
- Page load / initialization (default queries, preloaded data)
|
||||
- Search / filter / reset
|
||||
- CRUD operations (create, read, update, delete)
|
||||
- Table: pagination, sorting, row selection, bulk actions
|
||||
- Form submission & validation
|
||||
- Status transitions (e.g. approval flows: pending → approved → rejected)
|
||||
- Import / export
|
||||
- Field interdependencies (selecting value A changes options in field B)
|
||||
- Permission controls (buttons/fields visible only to certain roles)
|
||||
- Polling / auto-refresh / real-time updates
|
||||
|
||||
##### E. API Dependencies
|
||||
|
||||
**Case 1: API is integrated** (real HTTP calls in code)
|
||||
|
||||
| API Name | Method | Path | Trigger | Key Params | Notes |
|
||||
|----------|--------|------|---------|-----------|-------|
|
||||
| Get users | GET | /api/user/list | Load, search | page, size, keyword | Paginated |
|
||||
|
||||
**Case 2: API not integrated** (mock/hardcoded data)
|
||||
|
||||
When the page uses mock data, hardcoded fixtures, `setTimeout` simulations, or `Promise.resolve()` stubs — the API isn't real yet. **Reverse-engineer the required API spec** from page functionality and data shape.
|
||||
|
||||
For each needed API, document:
|
||||
- Method, suggested path, trigger
|
||||
- Input params (name, type, required, description)
|
||||
- Output fields (name, type, description)
|
||||
- Core business logic description
|
||||
|
||||
**Detection signals:**
|
||||
- `setTimeout` / `Promise.resolve()` returning data → mock
|
||||
- Data defined in component or `*.mock.*` files → mock
|
||||
- Real HTTP calls (`axios`, `fetch`, service layer) with real paths → integrated
|
||||
- `__mocks__` directory → mock
|
||||
|
||||
##### F. Page Relationships
|
||||
|
||||
- **Inbound**: Which pages link here? What parameters do they pass?
|
||||
- **Outbound**: Where can users navigate from here? What parameters?
|
||||
- **Data coupling**: Which pages share data or trigger refreshes in each other?
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Generate Documentation
|
||||
|
||||
#### Output Structure
|
||||
|
||||
Create `prd/` in project root (or user-specified directory):
|
||||
|
||||
```
|
||||
prd/
|
||||
├── README.md # System overview
|
||||
├── pages/
|
||||
│ ├── 01-user-mgmt-list.md # One file per page
|
||||
│ ├── 02-user-mgmt-detail.md
|
||||
│ ├── 03-order-mgmt-list.md
|
||||
│ └── ...
|
||||
└── appendix/
|
||||
├── enum-dictionary.md # All enums, status codes, type mappings
|
||||
├── page-relationships.md # Navigation map between pages
|
||||
└── api-inventory.md # Complete API reference
|
||||
```
|
||||
|
||||
#### README.md Template
|
||||
|
||||
```markdown
|
||||
# [System Name] — Product Requirements Document
|
||||
|
||||
## System Overview
|
||||
[2-3 paragraphs: what the system does, business context, primary users]
|
||||
|
||||
## Module Overview
|
||||
|
||||
| Module | Pages | Core Functionality |
|
||||
|--------|-------|--------------------|
|
||||
| User Management | User list, User detail, Role mgmt | CRUD users, assign roles and permissions |
|
||||
|
||||
## Page Inventory
|
||||
|
||||
| # | Page Name | Route | Module | Doc Link |
|
||||
|---|-----------|-------|--------|----------|
|
||||
| 1 | User List | /user/list | User Mgmt | [→](./pages/01-user-mgmt-list.md) |
|
||||
|
||||
## Global Notes
|
||||
|
||||
### Permission Model
|
||||
[Summarize auth/role system if present in code]
|
||||
|
||||
### Common Interaction Patterns
|
||||
[Global rules: all deletes require confirmation, lists default to created_at desc, etc.]
|
||||
```
|
||||
|
||||
#### Per-Page Document Template
|
||||
|
||||
```markdown
|
||||
# [Page Name]
|
||||
|
||||
> **Route:** `/xxx/xxx`
|
||||
> **Module:** [Module name]
|
||||
> **Generated:** [Date]
|
||||
|
||||
## Overview
|
||||
[2-3 sentences: core function and use case]
|
||||
|
||||
## Layout
|
||||
[Region breakdown — text description or ASCII diagram]
|
||||
|
||||
## Fields
|
||||
|
||||
### [Region: e.g. "Search Filters"]
|
||||
| Field | Type | Required | Options / Enum | Default | Notes |
|
||||
|-------|------|----------|---------------|---------|-------|
|
||||
|
||||
### [Region: e.g. "Data Table"]
|
||||
| Column | Format | Sortable | Filterable | Notes |
|
||||
|--------|--------|----------|-----------|-------|
|
||||
|
||||
### [Region: e.g. "Actions"]
|
||||
| Button | Visibility Condition | Behavior |
|
||||
|--------|---------------------|----------|
|
||||
|
||||
## Interactions
|
||||
|
||||
### Page Load
|
||||
[What happens on mount]
|
||||
|
||||
### [Scenario: e.g. "Search"]
|
||||
- **Trigger:** [User action]
|
||||
- **Behavior:** [System response]
|
||||
- **Special rules:** [If any]
|
||||
|
||||
### [Scenario: e.g. "Create"]
|
||||
- **Trigger:** ...
|
||||
- **Modal/drawer content:** [Fields and logic inside]
|
||||
- **Validation:** ...
|
||||
- **On success:** ...
|
||||
|
||||
## API Dependencies
|
||||
|
||||
| API | Method | Path | Trigger | Notes |
|
||||
|-----|--------|------|---------|-------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
## Page Relationships
|
||||
- **From:** [Source pages + params]
|
||||
- **To:** [Target pages + params]
|
||||
- **Data coupling:** [Cross-page refresh triggers]
|
||||
|
||||
## Business Rules
|
||||
[Anything that doesn't fit above]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Principles
|
||||
|
||||
### 1. Business Language First
|
||||
Don't write "calls `useState` to manage loading state." Write "search button shows a spinner to prevent duplicate submissions."
|
||||
|
||||
Don't write "useEffect fetches on mount." Write "page automatically loads the first page of results on open."
|
||||
|
||||
Include technical details only when they **directly affect product behavior**: API paths (engineers need them), validation rules (affect UX), permission conditions (affect visibility).
|
||||
|
||||
### 2. Don't Miss Hidden Logic
|
||||
Code contains logic PMs may not realize exists:
|
||||
- Field interdependencies (type A shows field X; type B shows field Y)
|
||||
- Conditional button visibility
|
||||
- Data formatting (currency with 2 decimals, date formats, status label mappings)
|
||||
- Default sort order and page size
|
||||
- Debounce/throttle effects on user input
|
||||
- Polling / auto-refresh intervals
|
||||
|
||||
### 3. Exhaustively List Enums
|
||||
When code defines enums (status codes, type codes, role types), list **every value and its meaning**. These are often scattered across constants files, component `valueEnum` configs, or API response mappers.
|
||||
|
||||
### 4. Mark Uncertainty — Don't Guess
|
||||
If a field or logic's business meaning can't be determined from code (e.g. abbreviated variable names, overly complex conditionals), mark it `[TBC]` and explain what you observed and why you're uncertain. Never fabricate business meaning.
|
||||
|
||||
### 5. Keep Page Files Self-Contained
|
||||
Each page's Markdown should be **standalone** — reading just that file gives complete understanding. Use relative links when referencing other pages or appendix entries.
|
||||
|
||||
---
|
||||
|
||||
## Page Type Strategies
|
||||
|
||||
### Frontend Pages
|
||||
|
||||
| Page Type | Focus Areas |
|
||||
|-----------|------------|
|
||||
| **List / Table** | Search conditions, columns, row actions, pagination, bulk ops |
|
||||
| **Form / Create-Edit** | Every field, validation, interdependencies, post-submit behavior |
|
||||
| **Detail / View** | Displayed info, tab/section organization, available actions |
|
||||
| **Modal / Drawer** | Describe as part of triggering page — not a separate file. But fully document content |
|
||||
| **Dashboard** | Data cards, charts, metrics meaning, filter dimensions, refresh frequency |
|
||||
|
||||
### Backend Endpoints (NestJS / Django / Express)
|
||||
|
||||
| Endpoint Type | Focus Areas |
|
||||
|---------------|------------|
|
||||
| **CRUD resource** | All fields (from DTO/serializer), validation rules, permissions, pagination, filtering, sorting |
|
||||
| **Auth endpoints** | Login/register flow, token format, refresh logic, password reset, OAuth providers |
|
||||
| **File upload** | Accepted types, size limits, storage destination, processing pipeline |
|
||||
| **Webhook / event** | Trigger conditions, payload shape, retry policy, idempotency |
|
||||
| **Background job** | Trigger, schedule, input/output, failure handling, monitoring |
|
||||
| **Admin views** (Django) | Registered models, list_display, search_fields, filters, inline models, custom actions |
|
||||
|
||||
---
|
||||
|
||||
## Execution Pacing
|
||||
|
||||
**Large projects (>15 pages):** Work in batches of 3-5 pages per module. Complete system overview + page inventory first. Output each batch for user review before proceeding.
|
||||
|
||||
**Small projects (≤15 pages):** Complete all analysis in one pass.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Pitfall | Fix |
|
||||
|---------|-----|
|
||||
| Using component names as page names | `UserManagementTable` → "User Management List" |
|
||||
| Skipping modals and drawers | They contain critical business logic — document fully |
|
||||
| Missing i18n field names | Check translation files, not just component JSX |
|
||||
| Ignoring dynamic route params | `/order/:id` = page requires an order ID to load |
|
||||
| Forgetting permission controls | Document which roles see which buttons/pages |
|
||||
| Assuming all APIs are real | Check for mock data patterns before documenting endpoints |
|
||||
| Skipping Django admin customization | `admin.py` often contains critical business rules (list filters, custom actions, inlines) |
|
||||
| Missing NestJS guards/pipes | `@UseGuards`, `@UsePipes` contain auth and validation logic that affects behavior |
|
||||
| Ignoring database constraints | Model field constraints (unique, max_length, choices) are validation rules for the PRD |
|
||||
| Overlooking middleware | Auth middleware, rate limiters, and CORS config define system-wide behavior |
|
||||
|
||||
---
|
||||
|
||||
## Tooling
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|-------|
|
||||
| `scripts/codebase_analyzer.py` | Scan codebase → extract routes, APIs, models, enums, structure | `python3 codebase_analyzer.py /path/to/project` |
|
||||
| `scripts/prd_scaffolder.py` | Generate PRD directory skeleton from analysis JSON | `python3 prd_scaffolder.py analysis.json` |
|
||||
|
||||
**Recommended workflow:**
|
||||
```bash
|
||||
# 1. Analyze the project (JSON output — works for frontend, backend, or fullstack)
|
||||
python3 scripts/codebase_analyzer.py /path/to/project -o analysis.json
|
||||
|
||||
# 2. Review the analysis (markdown summary)
|
||||
python3 scripts/codebase_analyzer.py /path/to/project -f markdown
|
||||
|
||||
# 3. Scaffold the PRD directory with stubs
|
||||
python3 scripts/prd_scaffolder.py analysis.json -o prd/ -n "My App"
|
||||
|
||||
# 4. Fill in TODO sections page-by-page using the SKILL.md workflow
|
||||
```
|
||||
|
||||
Both scripts are **stdlib-only** — no pip install needed.
|
||||
|
||||
### References
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `references/prd-quality-checklist.md` | Validation checklist for completeness, accuracy, readability |
|
||||
| `references/framework-patterns.md` | Framework-specific patterns for routes, state, APIs, forms, permissions |
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
This skill was inspired by [code-to-prd](https://github.com/lihanglogan/code-to-prd) by [@lihanglogan](https://github.com/lihanglogan), who proposed the original concept and methodology in [PR #368](https://github.com/alirezarezvani/claude-skills/pull/368). The core three-phase workflow (global scan → page-by-page analysis → structured document generation) originated from that work. This version was rebuilt from scratch in English with added tooling (analysis scripts, scaffolder, framework reference, quality checklist).
|
||||
81
product-team/code-to-prd/assets/sample-analysis.json
Normal file
81
product-team/code-to-prd/assets/sample-analysis.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"project": {
|
||||
"root": "/path/to/my-app",
|
||||
"name": "my-app",
|
||||
"framework": "next",
|
||||
"detected_frameworks": ["next", "react"],
|
||||
"key_dependencies": {
|
||||
"next": "14.1.0",
|
||||
"react": "18.2.0",
|
||||
"tailwindcss": "3.4.1",
|
||||
"axios": "1.6.5",
|
||||
"@tanstack/react-query": "5.17.0"
|
||||
},
|
||||
"stack_type": "fullstack"
|
||||
},
|
||||
"structure": {
|
||||
"total_files": 87,
|
||||
"components": {
|
||||
"components": 42,
|
||||
"modules": 35
|
||||
},
|
||||
"route_dirs": ["/path/to/my-app/app"],
|
||||
"api_dirs": ["/path/to/my-app/app/api"],
|
||||
"state_dirs": ["/path/to/my-app/src/store"],
|
||||
"i18n_dirs": [],
|
||||
"controller_dirs": [],
|
||||
"model_dirs": [],
|
||||
"dto_dirs": []
|
||||
},
|
||||
"routes": {
|
||||
"count": 8,
|
||||
"frontend_pages": [
|
||||
{"path": "/", "source": "app/page.tsx", "filesystem": true},
|
||||
{"path": "/dashboard", "source": "app/dashboard/page.tsx", "filesystem": true},
|
||||
{"path": "/users", "source": "app/users/page.tsx", "filesystem": true},
|
||||
{"path": "/users/:id", "source": "app/users/[id]/page.tsx", "filesystem": true},
|
||||
{"path": "/settings", "source": "app/settings/page.tsx", "filesystem": true}
|
||||
],
|
||||
"backend_endpoints": [
|
||||
{"path": "/api/users", "method": "GET", "source": "app/api/users/route.ts", "type": "backend"},
|
||||
{"path": "/api/users", "method": "POST", "source": "app/api/users/route.ts", "type": "backend"},
|
||||
{"path": "/api/users/:id", "method": "GET", "source": "app/api/users/[id]/route.ts", "type": "backend"}
|
||||
],
|
||||
"pages": []
|
||||
},
|
||||
"apis": {
|
||||
"total": 5,
|
||||
"integrated": 4,
|
||||
"mock": 1,
|
||||
"endpoints": [
|
||||
{"path": "/api/users", "method": "GET", "source": "services/user.ts", "integrated": true, "mock_detected": false},
|
||||
{"path": "/api/users", "method": "POST", "source": "services/user.ts", "integrated": true, "mock_detected": false},
|
||||
{"path": "/api/users/:id", "method": "GET", "source": "services/user.ts", "integrated": true, "mock_detected": false},
|
||||
{"path": "/api/users/:id", "method": "PUT", "source": "services/user.ts", "integrated": true, "mock_detected": false},
|
||||
{"path": "/api/dashboard/stats", "method": "GET", "source": "services/dashboard.ts", "integrated": false, "mock_detected": true}
|
||||
]
|
||||
},
|
||||
"enums": {
|
||||
"count": 2,
|
||||
"definitions": [
|
||||
{"name": "UserRole", "type": "enum", "values": {"ADMIN": "admin", "USER": "user", "MANAGER": "manager"}, "source": "types/user.ts"},
|
||||
{"name": "STATUS_MAP", "type": "constant_map", "values": {"active": "Active", "inactive": "Inactive", "suspended": "Suspended"}, "source": "constants/status.ts"}
|
||||
]
|
||||
},
|
||||
"models": {
|
||||
"count": 0,
|
||||
"definitions": []
|
||||
},
|
||||
"summary": {
|
||||
"pages": 5,
|
||||
"backend_endpoints": 3,
|
||||
"api_endpoints": 5,
|
||||
"api_integrated": 4,
|
||||
"api_mock": 1,
|
||||
"enums": 2,
|
||||
"models": 0,
|
||||
"has_i18n": false,
|
||||
"has_state_management": true,
|
||||
"stack_type": "fullstack"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Enum Dictionary
|
||||
|
||||
All enums, status codes, and constant mappings found in the codebase.
|
||||
|
||||
## UserRole
|
||||
|
||||
**Source:** `types/user.ts`
|
||||
**Type:** TypeScript enum
|
||||
|
||||
| Value | Label | Description |
|
||||
|-------|-------|-------------|
|
||||
| `admin` | Admin | Full system access, can manage all users |
|
||||
| `manager` | Manager | Can view and edit users, cannot delete |
|
||||
| `user` | User | Read-only access |
|
||||
|
||||
## STATUS_MAP
|
||||
|
||||
**Source:** `constants/status.ts`
|
||||
**Type:** Constant map
|
||||
|
||||
| Key | Display Value | Color | Description |
|
||||
|-----|--------------|-------|-------------|
|
||||
| `active` | Active | Green | Normal active account |
|
||||
| `inactive` | Inactive | Gray | Account disabled by user |
|
||||
| `suspended` | Suspended | Red | Account suspended by admin |
|
||||
@@ -0,0 +1,83 @@
|
||||
# User List
|
||||
|
||||
> **Route:** `/users`
|
||||
> **Module:** User Management
|
||||
> **Generated:** 2026-03-17
|
||||
|
||||
## Overview
|
||||
|
||||
Displays all system users in a searchable, paginated table. Supports creating, editing, and deleting users. Only ADMIN and MANAGER roles can access this page.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Top bar**: Search input + "Create User" button
|
||||
- **Main area**: Data table with pagination
|
||||
- **Modal**: Create/Edit user form (triggered by buttons)
|
||||
|
||||
## Fields
|
||||
|
||||
### Search Filters
|
||||
|
||||
| Field | Type | Required | Options | Default | Notes |
|
||||
|-------|------|----------|---------|---------|-------|
|
||||
| Keyword | Text input | No | — | — | Searches name and email |
|
||||
| Role | Select dropdown | No | Admin, Manager, User | All | Filters by role |
|
||||
| Status | Select dropdown | No | Active, Inactive, Suspended | All | Filters by status |
|
||||
|
||||
### Data Table
|
||||
|
||||
| Column | Format | Sortable | Filterable | Notes |
|
||||
|--------|--------|----------|-----------|-------|
|
||||
| Name | Text | Yes | No | Full name |
|
||||
| Email | Text (link) | Yes | No | Clickable → opens detail |
|
||||
| Role | Badge | No | Yes | Color-coded by role |
|
||||
| Status | Badge | No | Yes | Green=active, Red=suspended |
|
||||
| Created | Date (YYYY-MM-DD) | Yes | No | — |
|
||||
| Actions | Buttons | No | No | Edit, Delete |
|
||||
|
||||
### Actions
|
||||
|
||||
| Button | Visibility | Behavior |
|
||||
|--------|-----------|----------|
|
||||
| Create User | ADMIN, MANAGER | Opens create modal |
|
||||
| Edit | ADMIN, MANAGER | Opens edit modal with prefilled data |
|
||||
| Delete | ADMIN only | Confirmation dialog → soft delete |
|
||||
|
||||
## Interactions
|
||||
|
||||
### Page Load
|
||||
- Fetches first page of users via `GET /api/users?page=1&size=20`
|
||||
- Default sort: `created_at` descending
|
||||
|
||||
### Search
|
||||
- **Trigger:** User types in search field (300ms debounce)
|
||||
- **Behavior:** Re-fetches users with `keyword` param, resets to page 1
|
||||
- **Special rules:** Minimum 2 characters to trigger search
|
||||
|
||||
### Create User
|
||||
- **Trigger:** Click "Create User" button
|
||||
- **Modal content:** Name (required, max 50), Email (required, email format), Role (required, select), Status (default: Active)
|
||||
- **Validation:** Name required + max length, Email required + format check
|
||||
- **API:** `POST /api/users` with form data
|
||||
- **On success:** Toast "User created", close modal, refresh list
|
||||
- **On failure:** Show API error below form
|
||||
|
||||
### Delete User
|
||||
- **Trigger:** Click "Delete" button on row
|
||||
- **Behavior:** Confirmation dialog "Are you sure you want to delete {name}?"
|
||||
- **API:** `DELETE /api/users/:id`
|
||||
- **On success:** Toast "User deleted", refresh list
|
||||
|
||||
## API Dependencies
|
||||
|
||||
| API | Method | Path | Trigger | Notes |
|
||||
|-----|--------|------|---------|-------|
|
||||
| List users | GET | /api/users | Load, search, paginate | Params: page, size, keyword, role, status |
|
||||
| Create user | POST | /api/users | Submit create form | Body: name, email, role |
|
||||
| Delete user | DELETE | /api/users/:id | Confirm delete | — |
|
||||
|
||||
## Page Relationships
|
||||
|
||||
- **From:** Dashboard (click "View Users" link)
|
||||
- **To:** User Detail (click email or row)
|
||||
- **Data coupling:** Creating/deleting a user triggers dashboard stats refresh
|
||||
@@ -0,0 +1,43 @@
|
||||
# My App — Product Requirements Document
|
||||
|
||||
## System Overview
|
||||
|
||||
My App is a user management platform for internal teams. It provides CRUD operations for users, a dashboard with key metrics, and system settings. Built with Next.js 14 (App Router) and Tailwind CSS.
|
||||
|
||||
## Module Overview
|
||||
|
||||
| Module | Pages | Core Functionality |
|
||||
|--------|-------|--------------------|
|
||||
| Dashboard | Dashboard | Key metrics, activity feed |
|
||||
| User Management | User list, User detail | CRUD users, role assignment |
|
||||
| Settings | Settings | System configuration |
|
||||
|
||||
## Page Inventory
|
||||
|
||||
| # | Page Name | Route | Module | Doc Link |
|
||||
|---|-----------|-------|--------|----------|
|
||||
| 1 | Home | / | — | [→](./pages/01-home.md) |
|
||||
| 2 | Dashboard | /dashboard | Dashboard | [→](./pages/02-dashboard.md) |
|
||||
| 3 | User List | /users | User Mgmt | [→](./pages/03-user-list.md) |
|
||||
| 4 | User Detail | /users/:id | User Mgmt | [→](./pages/04-user-detail.md) |
|
||||
| 5 | Settings | /settings | Settings | [→](./pages/05-settings.md) |
|
||||
|
||||
## API Inventory
|
||||
|
||||
| # | Method | Path | Status | Notes |
|
||||
|---|--------|------|--------|-------|
|
||||
| 1 | GET | /api/users | Integrated | Paginated list |
|
||||
| 2 | POST | /api/users | Integrated | Create user |
|
||||
| 3 | GET | /api/users/:id | Integrated | User detail |
|
||||
| 4 | PUT | /api/users/:id | Integrated | Update user |
|
||||
| 5 | GET | /api/dashboard/stats | Mock | Dashboard metrics |
|
||||
|
||||
## Global Notes
|
||||
|
||||
### Permission Model
|
||||
Role-based access: ADMIN (full access), MANAGER (read + edit), USER (read-only).
|
||||
|
||||
### Common Interaction Patterns
|
||||
- All delete operations require confirmation modal
|
||||
- Lists default to `created_at` descending, 20 items per page
|
||||
- Form validation shows inline errors below each field
|
||||
228
product-team/code-to-prd/references/framework-patterns.md
Normal file
228
product-team/code-to-prd/references/framework-patterns.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Framework-Specific Patterns
|
||||
|
||||
Quick reference for identifying routes, components, state, and APIs across frontend and backend frameworks.
|
||||
|
||||
## React (CRA / Vite)
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `react-router-dom` — `<Route path="...">` or `createBrowserRouter` |
|
||||
| Components | `.tsx` / `.jsx` files, default exports |
|
||||
| State | Redux (`store/`), Zustand, Jotai, Recoil, React Context |
|
||||
| API | `axios`, `fetch`, TanStack Query (`useQuery`), SWR (`useSWR`) |
|
||||
| Forms | React Hook Form, Formik, Ant Design Form, custom `useState` |
|
||||
| i18n | `react-i18next`, `react-intl` |
|
||||
|
||||
## Next.js (App Router)
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `app/` directory — `page.tsx` = route, folders = segments |
|
||||
| Layouts | `layout.tsx` per directory |
|
||||
| Loading | `loading.tsx`, `error.tsx`, `not-found.tsx` |
|
||||
| API routes | `app/api/` or `pages/api/` (Pages Router) |
|
||||
| Server actions | `"use server"` directive |
|
||||
| Middleware | `middleware.ts` at root |
|
||||
|
||||
## Next.js (Pages Router)
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `pages/` directory — filename = route |
|
||||
| Data fetching | `getServerSideProps`, `getStaticProps`, `getStaticPaths` |
|
||||
| API routes | `pages/api/` |
|
||||
|
||||
## Vue 3
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `vue-router` — `routes` array in `router/index.ts` |
|
||||
| Components | `.vue` SFCs (`<template>`, `<script setup>`, `<style>`) |
|
||||
| State | Pinia (`stores/`), Vuex (`store/`) |
|
||||
| API | `axios`, `fetch`, VueQuery |
|
||||
| Forms | VeeValidate, FormKit, custom `ref()` / `reactive()` |
|
||||
| i18n | `vue-i18n` |
|
||||
|
||||
## Nuxt 3
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `pages/` directory (file-system routing) |
|
||||
| Layouts | `layouts/` |
|
||||
| API routes | `server/api/` |
|
||||
| Data fetching | `useFetch`, `useAsyncData`, `$fetch` |
|
||||
| State | `useState`, Pinia |
|
||||
| Middleware | `middleware/` |
|
||||
|
||||
## Angular
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `app-routing.module.ts` or `Routes` array |
|
||||
| Components | `@Component` decorator, `*.component.ts` |
|
||||
| State | NgRx (`store/`), services with `BehaviorSubject` |
|
||||
| API | `HttpClient` in services |
|
||||
| Forms | Reactive Forms (`FormGroup`), Template-driven forms |
|
||||
| i18n | `@angular/localize`, `ngx-translate` |
|
||||
| Guards | `CanActivate`, `CanDeactivate` |
|
||||
|
||||
## Svelte / SvelteKit
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `src/routes/` (file-system routing with `+page.svelte`) |
|
||||
| Layouts | `+layout.svelte` |
|
||||
| Data loading | `+page.ts` / `+page.server.ts` (`load` function) |
|
||||
| API routes | `+server.ts` |
|
||||
| State | Svelte stores (`writable`, `readable`, `derived`) |
|
||||
|
||||
## NestJS
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `@Controller('prefix')` + `@Get()/@Post()/@Put()/@Delete()` decorators |
|
||||
| Modules | `*.module.ts` — `@Module({ controllers, providers, imports })` |
|
||||
| Services | `*.service.ts` — injected via constructor, contains business logic |
|
||||
| DTOs | `*.dto.ts` — `class-validator` decorators define validation rules |
|
||||
| Entities | `*.entity.ts` — TypeORM `@Entity()` / Prisma schemas |
|
||||
| Auth | `@UseGuards(AuthGuard)`, `@Roles('admin')`, Passport strategies |
|
||||
| Middleware | `*.middleware.ts`, registered in module `configure()` |
|
||||
| Pipes | `ValidationPipe`, `ParseIntPipe` — input transformation |
|
||||
| Config | `ConfigModule`, `.env` files, `config/` directory |
|
||||
|
||||
## Express / Fastify
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `router.get('/path', handler)`, `app.post('/path', ...)` |
|
||||
| Middleware | `app.use(...)`, `router.use(...)` |
|
||||
| Controllers | Route handler files in `routes/`, `controllers/` |
|
||||
| Models | Mongoose schemas (`*.model.ts`), Sequelize models, Prisma |
|
||||
| Auth | `passport`, `jsonwebtoken`, middleware auth checks |
|
||||
| Validation | `express-validator`, `joi`, `zod`, custom middleware |
|
||||
|
||||
## Django
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `urls.py` — `urlpatterns = [path('...', view)]` |
|
||||
| Views | `views.py` — function-based or class-based views (`APIView`, `ViewSet`) |
|
||||
| Models | `models.py` — `class MyModel(models.Model)` with field definitions |
|
||||
| Forms | `forms.py` — `ModelForm`, `Form` with validation |
|
||||
| Serializers | `serializers.py` (DRF) — `ModelSerializer`, field-level validation |
|
||||
| Admin | `admin.py` — `@admin.register`, `list_display`, `search_fields`, `list_filter` |
|
||||
| Templates | `templates/` — Jinja2/Django template HTML files |
|
||||
| Middleware | `MIDDLEWARE` in `settings.py` |
|
||||
| Auth | `django.contrib.auth`, `rest_framework.permissions`, `@login_required` |
|
||||
| Signals | `signals.py` — `post_save`, `pre_delete` hooks (hidden business logic) |
|
||||
| Management commands | `management/commands/` — CLI operations |
|
||||
| Celery tasks | `tasks.py` — async/background operations |
|
||||
|
||||
## Django REST Framework (DRF)
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Endpoints | `router.register('prefix', ViewSet)` in `urls.py` |
|
||||
| ViewSets | `viewsets.py` — `ModelViewSet` (full CRUD), `ReadOnlyModelViewSet` |
|
||||
| Serializers | `serializers.py` — field types, validators, nested relations |
|
||||
| Permissions | `permission_classes = [IsAuthenticated, IsAdminUser]` |
|
||||
| Filtering | `django-filter`, `search_fields`, `ordering_fields` |
|
||||
| Pagination | `DEFAULT_PAGINATION_CLASS` in settings, per-view override |
|
||||
| Throttling | `DEFAULT_THROTTLE_CLASSES`, per-view `throttle_classes` |
|
||||
|
||||
## FastAPI
|
||||
|
||||
| Aspect | Where to Look |
|
||||
|--------|--------------|
|
||||
| Routes | `@app.get('/path')`, `@router.post('/path')` decorators |
|
||||
| Models | Pydantic `BaseModel` classes — request/response schemas |
|
||||
| Dependencies | `Depends(...)` — auth, DB sessions, shared logic |
|
||||
| DB | SQLAlchemy models, Tortoise ORM, or raw SQL |
|
||||
| Auth | `OAuth2PasswordBearer`, JWT middleware, `Depends(get_current_user)` |
|
||||
| Background | `BackgroundTasks`, Celery integration |
|
||||
|
||||
## Common Patterns Across Frameworks
|
||||
|
||||
### Mock Detection
|
||||
```
|
||||
# Likely mock
|
||||
setTimeout(() => resolve(data), 500)
|
||||
Promise.resolve(mockData)
|
||||
import { data } from './fixtures'
|
||||
faker.name.firstName()
|
||||
|
||||
# Likely real
|
||||
axios.get('/api/users')
|
||||
fetch('/api/data')
|
||||
httpClient.post(url, body)
|
||||
useSWR('/api/resource')
|
||||
```
|
||||
|
||||
### Permission Patterns
|
||||
```
|
||||
# React
|
||||
{hasPermission('admin') && <Button>Delete</Button>}
|
||||
<ProtectedRoute roles={['admin', 'manager']}>
|
||||
|
||||
# Vue
|
||||
v-if="user.role === 'admin'"
|
||||
v-permission="'user:delete'"
|
||||
|
||||
# Angular
|
||||
*ngIf="authService.hasRole('admin')"
|
||||
canActivate: [AuthGuard]
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
```
|
||||
# React Hook Form
|
||||
{ required: 'Name is required', maxLength: { value: 50, message: 'Too long' } }
|
||||
|
||||
# VeeValidate (Vue)
|
||||
rules="required|email|max:100"
|
||||
|
||||
# Angular Reactive Forms
|
||||
Validators.required, Validators.minLength(3), Validators.pattern(...)
|
||||
|
||||
# NestJS (class-validator)
|
||||
@IsString() @IsNotEmpty() @MaxLength(50) name: string;
|
||||
@IsEmail() email: string;
|
||||
@IsEnum(UserRole) role: UserRole;
|
||||
|
||||
# Django Forms
|
||||
name = forms.CharField(max_length=50, required=True)
|
||||
email = forms.EmailField()
|
||||
|
||||
# DRF Serializers
|
||||
name = serializers.CharField(max_length=50)
|
||||
email = serializers.EmailField(required=True)
|
||||
|
||||
# FastAPI (Pydantic)
|
||||
name: str = Field(max_length=50)
|
||||
email: EmailStr
|
||||
```
|
||||
|
||||
### Database Model Patterns
|
||||
```
|
||||
# Django
|
||||
class Order(models.Model):
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
total = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
# TypeORM (NestJS)
|
||||
@Entity()
|
||||
export class Order {
|
||||
@Column({ type: 'enum', enum: OrderStatus })
|
||||
status: OrderStatus;
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
}
|
||||
|
||||
# Prisma
|
||||
model Order {
|
||||
status OrderStatus
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
total Decimal
|
||||
}
|
||||
```
|
||||
65
product-team/code-to-prd/references/prd-quality-checklist.md
Normal file
65
product-team/code-to-prd/references/prd-quality-checklist.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# PRD Quality Checklist
|
||||
|
||||
Use this checklist to validate generated PRDs before delivery.
|
||||
|
||||
## Completeness
|
||||
|
||||
- [ ] Every route/page has a corresponding document
|
||||
- [ ] All form fields listed with type, required, validation, default
|
||||
- [ ] All table columns listed with format, sortable, filterable
|
||||
- [ ] All action buttons documented with visibility conditions
|
||||
- [ ] All API endpoints listed with method, path, trigger, params
|
||||
- [ ] Mock vs integrated APIs clearly distinguished
|
||||
- [ ] All enums exhaustively listed with every value
|
||||
- [ ] Page load behavior documented for every page
|
||||
- [ ] Page relationships mapped (inbound, outbound, data coupling)
|
||||
|
||||
## Accuracy
|
||||
|
||||
- [ ] Route paths match actual code
|
||||
- [ ] Field names match UI labels (not variable names)
|
||||
- [ ] Validation rules match actual code logic
|
||||
- [ ] Permission conditions match auth guard implementations
|
||||
- [ ] API paths match actual service layer calls
|
||||
- [ ] Enum values match source constants (no fabrication)
|
||||
- [ ] Uncertain items marked `[TBC]` with explanation
|
||||
|
||||
## Readability
|
||||
|
||||
- [ ] Business language used (not implementation details)
|
||||
- [ ] Each page doc is self-contained
|
||||
- [ ] No component names used as page names
|
||||
- [ ] Interactions described as user action → system response
|
||||
- [ ] Modals/drawers documented within their parent page
|
||||
- [ ] README system overview written for non-technical reader
|
||||
|
||||
## Structure
|
||||
|
||||
- [ ] `prd/README.md` exists with system overview + page inventory
|
||||
- [ ] `prd/pages/` contains numbered page files
|
||||
- [ ] `prd/appendix/enum-dictionary.md` exists
|
||||
- [ ] `prd/appendix/api-inventory.md` exists
|
||||
- [ ] `prd/appendix/page-relationships.md` exists
|
||||
- [ ] Cross-references use relative links
|
||||
|
||||
## Backend-Specific Checks
|
||||
|
||||
- [ ] All controller/view endpoints documented with method, path, auth
|
||||
- [ ] DTO/serializer fields listed with type, required, validation
|
||||
- [ ] Database model relationships mapped (FK, M2M, O2O)
|
||||
- [ ] Django admin customizations documented (list_display, actions, inlines)
|
||||
- [ ] Background tasks/Celery jobs documented with trigger and schedule
|
||||
- [ ] Middleware pipeline documented (auth, logging, rate limiting)
|
||||
- [ ] Environment-dependent behavior noted (dev vs prod differences)
|
||||
- [ ] Database migrations reviewed for field constraints and defaults
|
||||
|
||||
## Common Issues to Watch
|
||||
|
||||
| Issue | How to Detect | Fix |
|
||||
|-------|--------------|-----|
|
||||
| Missing modal content | Search for `Modal`, `Dialog`, `Drawer` components | Add as subsection in parent page |
|
||||
| Undocumented field linking | Search for conditional renders based on field values | Add to interaction logic |
|
||||
| Hidden permissions | Search for `v-if`, `v-show`, role checks, auth guards | Add visibility conditions |
|
||||
| Stale mock data | Compare mock shapes with API types/interfaces | Flag as `[Mock - verify with backend]` |
|
||||
| Missing error states | Search for error boundaries, catch blocks, toast errors | Add failure paths to interactions |
|
||||
| Unlinked pages | Cross-reference route params with navigation calls | Complete page relationships |
|
||||
1
product-team/code-to-prd/scripts/.gitignore
vendored
Normal file
1
product-team/code-to-prd/scripts/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
prd/
|
||||
732
product-team/code-to-prd/scripts/codebase_analyzer.py
Executable file
732
product-team/code-to-prd/scripts/codebase_analyzer.py
Executable file
@@ -0,0 +1,732 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze any codebase (frontend, backend, or fullstack) and extract routes, APIs, models, and structure.
|
||||
|
||||
Supports: React, Vue, Angular, Svelte, Next.js, Nuxt, NestJS, Express, Django, FastAPI, Flask.
|
||||
Stdlib only — no third-party dependencies. Outputs JSON for downstream PRD generation.
|
||||
|
||||
Usage:
|
||||
python3 codebase_analyzer.py /path/to/project
|
||||
python3 codebase_analyzer.py /path/to/project --output prd-analysis.json
|
||||
python3 codebase_analyzer.py /path/to/project --format markdown
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
IGNORED_DIRS = {
|
||||
".git", "node_modules", ".next", "dist", "build", "coverage",
|
||||
"venv", ".venv", "__pycache__", ".nuxt", ".output", ".cache",
|
||||
".turbo", ".vercel", "out", "storybook-static",
|
||||
".tox", ".mypy_cache", ".pytest_cache", "htmlcov", "staticfiles",
|
||||
"media", "migrations", "egg-info",
|
||||
}
|
||||
|
||||
FRAMEWORK_SIGNALS = {
|
||||
"react": ["react", "react-dom"],
|
||||
"next": ["next"],
|
||||
"vue": ["vue"],
|
||||
"nuxt": ["nuxt"],
|
||||
"angular": ["@angular/core"],
|
||||
"svelte": ["svelte"],
|
||||
"sveltekit": ["@sveltejs/kit"],
|
||||
"solid": ["solid-js"],
|
||||
"astro": ["astro"],
|
||||
"remix": ["@remix-run/react"],
|
||||
"nestjs": ["@nestjs/core"],
|
||||
"express": ["express"],
|
||||
"fastify": ["fastify"],
|
||||
}
|
||||
|
||||
# Python backend frameworks detected via project files (no package.json)
|
||||
PYTHON_FRAMEWORK_FILES = {
|
||||
"django": ["manage.py", "settings.py"],
|
||||
"fastapi": ["main.py"], # confirmed via imports
|
||||
"flask": ["app.py"], # confirmed via imports
|
||||
}
|
||||
|
||||
ROUTE_FILE_PATTERNS = [
|
||||
"**/router.{ts,tsx,js,jsx}",
|
||||
"**/routes.{ts,tsx,js,jsx}",
|
||||
"**/routing.{ts,tsx,js,jsx}",
|
||||
"**/app-routing*.{ts,tsx,js,jsx}",
|
||||
]
|
||||
|
||||
ROUTE_DIR_PATTERNS = [
|
||||
"pages", "views", "routes", "app",
|
||||
"src/pages", "src/views", "src/routes", "src/app",
|
||||
]
|
||||
|
||||
API_DIR_PATTERNS = [
|
||||
"api", "services", "requests", "endpoints", "client",
|
||||
"src/api", "src/services", "src/requests",
|
||||
]
|
||||
|
||||
STATE_DIR_PATTERNS = [
|
||||
"store", "stores", "models", "context", "state",
|
||||
"src/store", "src/stores", "src/models", "src/context",
|
||||
]
|
||||
|
||||
I18N_DIR_PATTERNS = [
|
||||
"locales", "i18n", "lang", "translations", "messages",
|
||||
"src/locales", "src/i18n", "src/lang",
|
||||
]
|
||||
|
||||
# Backend-specific directory patterns
|
||||
CONTROLLER_DIR_PATTERNS = [
|
||||
"controllers", "src/controllers", "src/modules",
|
||||
]
|
||||
|
||||
MODEL_DIR_PATTERNS = [
|
||||
"models", "entities", "src/entities", "src/models",
|
||||
]
|
||||
|
||||
DTO_DIR_PATTERNS = [
|
||||
"dto", "dtos", "src/dto", "serializers",
|
||||
]
|
||||
|
||||
MOCK_SIGNALS = [
|
||||
r"setTimeout\s*\(.*\breturn\b",
|
||||
r"Promise\.resolve\s*\(",
|
||||
r"\.mock\.",
|
||||
r"__mocks__",
|
||||
r"mockData",
|
||||
r"mock[A-Z]",
|
||||
r"faker\.",
|
||||
r"fixtures?/",
|
||||
]
|
||||
|
||||
REAL_API_SIGNALS = [
|
||||
r"\baxios\b",
|
||||
r"\bfetch\s*\(",
|
||||
r"httpGet|httpPost|httpPut|httpDelete|httpPatch",
|
||||
r"\.get\s*\(\s*['\"`/]",
|
||||
r"\.post\s*\(\s*['\"`/]",
|
||||
r"\.put\s*\(\s*['\"`/]",
|
||||
r"\.delete\s*\(\s*['\"`/]",
|
||||
r"\.patch\s*\(\s*['\"`/]",
|
||||
r"useSWR|useQuery|useMutation",
|
||||
r"\$http\.",
|
||||
r"this\.http\.",
|
||||
]
|
||||
|
||||
ROUTE_PATTERNS = [
|
||||
# React Router
|
||||
r'<Route\s+[^>]*path\s*=\s*["\']([^"\']+)["\']',
|
||||
r'path\s*:\s*["\']([^"\']+)["\']',
|
||||
# Vue Router
|
||||
r'path\s*:\s*["\']([^"\']+)["\']',
|
||||
# Angular
|
||||
r'path\s*:\s*["\']([^"\']+)["\']',
|
||||
]
|
||||
|
||||
API_PATH_PATTERNS = [
|
||||
r'["\'](?:GET|POST|PUT|DELETE|PATCH)["\'].*?["\'](/[a-zA-Z0-9/_\-:{}]+)["\']',
|
||||
r'(?:get|post|put|delete|patch)\s*\(\s*["\'](/[a-zA-Z0-9/_\-:{}]+)["\']',
|
||||
r'(?:url|path|endpoint|baseURL)\s*[:=]\s*["\'](/[a-zA-Z0-9/_\-:{}]+)["\']',
|
||||
r'fetch\s*\(\s*[`"\'](?:https?://[^/]+)?(/[a-zA-Z0-9/_\-:{}]+)',
|
||||
]
|
||||
|
||||
COMPONENT_EXTENSIONS = {".tsx", ".jsx", ".vue", ".svelte", ".astro"}
|
||||
CODE_EXTENSIONS = {".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".astro", ".py"}
|
||||
|
||||
# NestJS decorator patterns
|
||||
NEST_ROUTE_PATTERNS = [
|
||||
r"@(?:Get|Post|Put|Delete|Patch|Head|Options|All)\s*\(\s*['\"]([^'\"]*)['\"]",
|
||||
r"@Controller\s*\(\s*['\"]([^'\"]*)['\"]",
|
||||
]
|
||||
|
||||
# Django URL patterns
|
||||
DJANGO_ROUTE_PATTERNS = [
|
||||
r"path\s*\(\s*['\"]([^'\"]+)['\"]",
|
||||
r"url\s*\(\s*r?['\"]([^'\"]+)['\"]",
|
||||
r"register\s*\(\s*r?['\"]([^'\"]+)['\"]",
|
||||
]
|
||||
|
||||
# Django/Python model patterns
|
||||
PYTHON_MODEL_PATTERNS = [
|
||||
r"class\s+(\w+)\s*\(.*?models\.Model\)",
|
||||
r"class\s+(\w+)\s*\(.*?BaseModel\)", # Pydantic
|
||||
]
|
||||
|
||||
# NestJS entity/DTO patterns
|
||||
NEST_MODEL_PATTERNS = [
|
||||
r"@Entity\s*\(.*?\)\s*(?:export\s+)?class\s+(\w+)",
|
||||
r"class\s+(\w+(?:Dto|DTO|Entity|Schema))\b",
|
||||
]
|
||||
|
||||
|
||||
def detect_framework(project_root: Path) -> Dict[str, Any]:
|
||||
"""Detect framework from package.json (Node.js) or project files (Python)."""
|
||||
detected = []
|
||||
all_deps = {}
|
||||
pkg_name = ""
|
||||
pkg_version = ""
|
||||
|
||||
# Node.js detection via package.json
|
||||
pkg_path = project_root / "package.json"
|
||||
if pkg_path.exists():
|
||||
try:
|
||||
with open(pkg_path) as f:
|
||||
pkg = json.load(f)
|
||||
pkg_name = pkg.get("name", "")
|
||||
pkg_version = pkg.get("version", "")
|
||||
for key in ("dependencies", "devDependencies", "peerDependencies"):
|
||||
all_deps.update(pkg.get(key, {}))
|
||||
for framework, signals in FRAMEWORK_SIGNALS.items():
|
||||
if any(s in all_deps for s in signals):
|
||||
detected.append(framework)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
|
||||
# Python backend detection via project files and imports
|
||||
if (project_root / "manage.py").exists():
|
||||
detected.append("django")
|
||||
if (project_root / "requirements.txt").exists() or (project_root / "pyproject.toml").exists():
|
||||
for req_file in ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"]:
|
||||
req_path = project_root / req_file
|
||||
if req_path.exists():
|
||||
try:
|
||||
content = req_path.read_text(errors="replace").lower()
|
||||
if "django" in content and "django" not in detected:
|
||||
detected.append("django")
|
||||
if "fastapi" in content:
|
||||
detected.append("fastapi")
|
||||
if "flask" in content and "flask" not in detected:
|
||||
detected.append("flask")
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
# Prefer specific over generic
|
||||
priority = [
|
||||
"sveltekit", "next", "nuxt", "remix", "astro", # fullstack JS
|
||||
"nestjs", "express", "fastify", # backend JS
|
||||
"django", "fastapi", "flask", # backend Python
|
||||
"angular", "svelte", "vue", "react", "solid", # frontend JS
|
||||
]
|
||||
framework = "unknown"
|
||||
for fw in priority:
|
||||
if fw in detected:
|
||||
framework = fw
|
||||
break
|
||||
|
||||
return {
|
||||
"framework": framework,
|
||||
"name": pkg_name or project_root.name,
|
||||
"version": pkg_version,
|
||||
"detected_frameworks": detected,
|
||||
"dependency_count": len(all_deps),
|
||||
"key_deps": {k: v for k, v in all_deps.items()
|
||||
if any(s in k for s in ["router", "redux", "vuex", "pinia", "zustand",
|
||||
"mobx", "recoil", "jotai", "tanstack", "swr",
|
||||
"axios", "tailwind", "material", "ant",
|
||||
"chakra", "shadcn", "i18n", "intl",
|
||||
"typeorm", "prisma", "sequelize", "mongoose",
|
||||
"passport", "jwt", "class-validator"])},
|
||||
}
|
||||
|
||||
|
||||
def find_dirs(root: Path, patterns: List[str]) -> List[Path]:
|
||||
"""Find directories matching common patterns."""
|
||||
found = []
|
||||
for pattern in patterns:
|
||||
candidate = root / pattern
|
||||
if candidate.is_dir():
|
||||
found.append(candidate)
|
||||
return found
|
||||
|
||||
|
||||
def walk_files(root: Path, extensions: Set[str] = CODE_EXTENSIONS) -> List[Path]:
|
||||
"""Walk project tree, skip ignored dirs, return files matching extensions."""
|
||||
results = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in IGNORED_DIRS]
|
||||
for fname in filenames:
|
||||
if Path(fname).suffix in extensions:
|
||||
results.append(Path(dirpath) / fname)
|
||||
return results
|
||||
|
||||
|
||||
def extract_routes_from_file(filepath: Path) -> List[Dict[str, str]]:
|
||||
"""Extract route definitions from a file."""
|
||||
routes = []
|
||||
try:
|
||||
content = filepath.read_text(errors="replace")
|
||||
except IOError:
|
||||
return routes
|
||||
|
||||
for pattern in ROUTE_PATTERNS:
|
||||
for match in re.finditer(pattern, content):
|
||||
path = match.group(1)
|
||||
if path and not path.startswith("http") and len(path) < 200:
|
||||
routes.append({
|
||||
"path": path,
|
||||
"source": str(filepath),
|
||||
"line": content[:match.start()].count("\n") + 1,
|
||||
})
|
||||
return routes
|
||||
|
||||
|
||||
def extract_routes_from_filesystem(pages_dir: Path, root: Path) -> List[Dict[str, str]]:
|
||||
"""Infer routes from file-system routing (Next.js, Nuxt, SvelteKit)."""
|
||||
routes = []
|
||||
for filepath in sorted(pages_dir.rglob("*")):
|
||||
if filepath.is_file() and filepath.suffix in CODE_EXTENSIONS:
|
||||
rel = filepath.relative_to(pages_dir)
|
||||
route = "/" + str(rel.with_suffix("")).replace("\\", "/")
|
||||
# Normalize index routes
|
||||
route = re.sub(r"/index$", "", route) or "/"
|
||||
# Convert [param] to :param
|
||||
route = re.sub(r"\[\.\.\.(\w+)\]", r"*\1", route)
|
||||
route = re.sub(r"\[(\w+)\]", r":\1", route)
|
||||
routes.append({
|
||||
"path": route,
|
||||
"source": str(filepath),
|
||||
"filesystem": True,
|
||||
})
|
||||
return routes
|
||||
|
||||
|
||||
def extract_apis_from_file(filepath: Path) -> List[Dict[str, Any]]:
|
||||
"""Extract API calls from a file."""
|
||||
apis = []
|
||||
try:
|
||||
content = filepath.read_text(errors="replace")
|
||||
except IOError:
|
||||
return apis
|
||||
|
||||
is_mock = any(re.search(p, content) for p in MOCK_SIGNALS)
|
||||
is_real = any(re.search(p, content) for p in REAL_API_SIGNALS)
|
||||
|
||||
for pattern in API_PATH_PATTERNS:
|
||||
for match in re.finditer(pattern, content):
|
||||
path = match.group(1) if match.lastindex else match.group(0)
|
||||
if path and len(path) < 200:
|
||||
# Try to detect HTTP method
|
||||
context = content[max(0, match.start() - 100):match.end()]
|
||||
method = "UNKNOWN"
|
||||
for m in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
||||
if m.lower() in context.lower():
|
||||
method = m
|
||||
break
|
||||
|
||||
apis.append({
|
||||
"path": path,
|
||||
"method": method,
|
||||
"source": str(filepath),
|
||||
"line": content[:match.start()].count("\n") + 1,
|
||||
"integrated": is_real and not is_mock,
|
||||
"mock_detected": is_mock,
|
||||
})
|
||||
return apis
|
||||
|
||||
|
||||
def extract_enums(filepath: Path) -> List[Dict[str, Any]]:
|
||||
"""Extract enum/constant definitions."""
|
||||
enums = []
|
||||
try:
|
||||
content = filepath.read_text(errors="replace")
|
||||
except IOError:
|
||||
return enums
|
||||
|
||||
# TypeScript enums
|
||||
for match in re.finditer(r"enum\s+(\w+)\s*\{([^}]+)\}", content):
|
||||
name = match.group(1)
|
||||
body = match.group(2)
|
||||
values = re.findall(r"(\w+)\s*=\s*['\"]?([^,'\"\n]+)", body)
|
||||
enums.append({
|
||||
"name": name,
|
||||
"type": "enum",
|
||||
"values": {k.strip(): v.strip().rstrip(",") for k, v in values},
|
||||
"source": str(filepath),
|
||||
})
|
||||
|
||||
# Object constant maps (const STATUS_MAP = { ... })
|
||||
for match in re.finditer(
|
||||
r"(?:const|export\s+const)\s+(\w*(?:MAP|STATUS|TYPE|ENUM|OPTION|ROLE|STATE)\w*)\s*[:=]\s*\{([^}]+)\}",
|
||||
content, re.IGNORECASE
|
||||
):
|
||||
name = match.group(1)
|
||||
body = match.group(2)
|
||||
values = re.findall(r"['\"]?(\w+)['\"]?\s*:\s*['\"]([^'\"]+)['\"]", body)
|
||||
if values:
|
||||
enums.append({
|
||||
"name": name,
|
||||
"type": "constant_map",
|
||||
"values": dict(values),
|
||||
"source": str(filepath),
|
||||
})
|
||||
|
||||
return enums
|
||||
|
||||
|
||||
def extract_backend_routes(filepath: Path, framework: str) -> List[Dict[str, str]]:
|
||||
"""Extract route definitions from NestJS controllers or Django url configs."""
|
||||
routes = []
|
||||
try:
|
||||
content = filepath.read_text(errors="replace")
|
||||
except IOError:
|
||||
return routes
|
||||
|
||||
patterns = []
|
||||
if framework in ("nestjs", "express", "fastify"):
|
||||
patterns = NEST_ROUTE_PATTERNS
|
||||
elif framework == "django":
|
||||
patterns = DJANGO_ROUTE_PATTERNS
|
||||
|
||||
# For NestJS, also grab the controller prefix
|
||||
controller_prefix = ""
|
||||
if framework == "nestjs":
|
||||
m = re.search(r"@Controller\s*\(\s*['\"]([^'\"]*)['\"]", content)
|
||||
if m:
|
||||
controller_prefix = "/" + m.group(1).strip("/")
|
||||
|
||||
for pattern in patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
path = match.group(1)
|
||||
if not path or path.startswith("http") or len(path) > 200:
|
||||
continue
|
||||
# For NestJS method decorators, prepend controller prefix
|
||||
if framework == "nestjs" and not path.startswith("/"):
|
||||
full_path = f"{controller_prefix}/{path}".replace("//", "/")
|
||||
else:
|
||||
full_path = path if path.startswith("/") else f"/{path}"
|
||||
|
||||
# Detect HTTP method from decorator name
|
||||
method = "UNKNOWN"
|
||||
ctx = content[max(0, match.start() - 30):match.start()]
|
||||
for m_name in ["Get", "Post", "Put", "Delete", "Patch"]:
|
||||
if f"@{m_name}" in ctx or f"@{m_name.lower()}" in ctx:
|
||||
method = m_name.upper()
|
||||
break
|
||||
|
||||
routes.append({
|
||||
"path": full_path,
|
||||
"method": method,
|
||||
"source": str(filepath),
|
||||
"line": content[:match.start()].count("\n") + 1,
|
||||
"type": "backend",
|
||||
})
|
||||
return routes
|
||||
|
||||
|
||||
def extract_models(filepath: Path, framework: str) -> List[Dict[str, Any]]:
|
||||
"""Extract model/entity definitions from backend code."""
|
||||
models = []
|
||||
try:
|
||||
content = filepath.read_text(errors="replace")
|
||||
except IOError:
|
||||
return models
|
||||
|
||||
patterns = PYTHON_MODEL_PATTERNS if framework in ("django", "fastapi", "flask") else NEST_MODEL_PATTERNS
|
||||
for pattern in patterns:
|
||||
for match in re.finditer(pattern, content):
|
||||
name = match.group(1)
|
||||
# Try to extract fields
|
||||
fields = []
|
||||
# For Django models: field_name = models.FieldType(...)
|
||||
if framework == "django":
|
||||
block_start = match.end()
|
||||
block = content[block_start:block_start + 2000]
|
||||
for fm in re.finditer(
|
||||
r"(\w+)\s*=\s*models\.(\w+)\s*\(([^)]*)\)", block
|
||||
):
|
||||
fields.append({
|
||||
"name": fm.group(1),
|
||||
"type": fm.group(2),
|
||||
"args": fm.group(3).strip()[:100],
|
||||
})
|
||||
models.append({
|
||||
"name": name,
|
||||
"source": str(filepath),
|
||||
"framework": framework,
|
||||
"fields": fields,
|
||||
})
|
||||
return models
|
||||
|
||||
|
||||
def count_components(files: List[Path]) -> Dict[str, int]:
|
||||
"""Count components by type."""
|
||||
counts: Dict[str, int] = defaultdict(int)
|
||||
for f in files:
|
||||
if f.suffix in COMPONENT_EXTENSIONS:
|
||||
counts["components"] += 1
|
||||
elif f.suffix in {".ts", ".js"}:
|
||||
counts["modules"] += 1
|
||||
return dict(counts)
|
||||
|
||||
|
||||
def analyze_project(project_root: Path) -> Dict[str, Any]:
|
||||
"""Run full analysis on a frontend project."""
|
||||
root = Path(project_root).resolve()
|
||||
if not root.is_dir():
|
||||
return {"error": f"Not a directory: {root}"}
|
||||
|
||||
# 1. Framework detection
|
||||
framework_info = detect_framework(root)
|
||||
|
||||
# 2. File inventory
|
||||
all_files = walk_files(root)
|
||||
component_counts = count_components(all_files)
|
||||
|
||||
# 3. Directory structure
|
||||
route_dirs = find_dirs(root, ROUTE_DIR_PATTERNS)
|
||||
api_dirs = find_dirs(root, API_DIR_PATTERNS)
|
||||
state_dirs = find_dirs(root, STATE_DIR_PATTERNS)
|
||||
i18n_dirs = find_dirs(root, I18N_DIR_PATTERNS)
|
||||
|
||||
# 4. Routes (frontend + backend)
|
||||
routes = []
|
||||
fw = framework_info["framework"]
|
||||
|
||||
# Frontend: config-based routes
|
||||
for f in all_files:
|
||||
if any(p in f.name.lower() for p in ["router", "routes", "routing"]):
|
||||
routes.extend(extract_routes_from_file(f))
|
||||
|
||||
# Frontend: file-system routes (Next.js, Nuxt, SvelteKit)
|
||||
if fw in ("next", "nuxt", "sveltekit", "remix", "astro"):
|
||||
for d in route_dirs:
|
||||
routes.extend(extract_routes_from_filesystem(d, root))
|
||||
|
||||
# Backend: NestJS controllers, Django urls
|
||||
if fw in ("nestjs", "express", "fastify", "django"):
|
||||
for f in all_files:
|
||||
if fw == "django" and "urls.py" in f.name:
|
||||
routes.extend(extract_backend_routes(f, fw))
|
||||
elif fw in ("nestjs", "express", "fastify") and ".controller." in f.name:
|
||||
routes.extend(extract_backend_routes(f, fw))
|
||||
|
||||
# Deduplicate routes by path (+ method for backend)
|
||||
seen_paths: Set[str] = set()
|
||||
unique_routes = []
|
||||
for r in routes:
|
||||
key = r["path"] if r.get("type") != "backend" else f"{r.get('method', '')}:{r['path']}"
|
||||
if key not in seen_paths:
|
||||
seen_paths.add(key)
|
||||
unique_routes.append(r)
|
||||
routes = sorted(unique_routes, key=lambda r: r["path"])
|
||||
|
||||
# 5. API calls
|
||||
apis = []
|
||||
for f in all_files:
|
||||
apis.extend(extract_apis_from_file(f))
|
||||
|
||||
# Deduplicate APIs by path+method
|
||||
seen_apis: Set[Tuple[str, str]] = set()
|
||||
unique_apis = []
|
||||
for a in apis:
|
||||
key = (a["path"], a["method"])
|
||||
if key not in seen_apis:
|
||||
seen_apis.add(key)
|
||||
unique_apis.append(a)
|
||||
apis = sorted(unique_apis, key=lambda a: a["path"])
|
||||
|
||||
# 6. Enums
|
||||
enums = []
|
||||
for f in all_files:
|
||||
enums.extend(extract_enums(f))
|
||||
|
||||
# 7. Models/entities (backend)
|
||||
models = []
|
||||
if fw in ("django", "fastapi", "flask", "nestjs"):
|
||||
for f in all_files:
|
||||
if fw == "django" and "models.py" in f.name:
|
||||
models.extend(extract_models(f, fw))
|
||||
elif fw == "nestjs" and (".entity." in f.name or ".dto." in f.name):
|
||||
models.extend(extract_models(f, fw))
|
||||
|
||||
# Deduplicate models by name
|
||||
seen_models: Set[str] = set()
|
||||
unique_models = []
|
||||
for m in models:
|
||||
if m["name"] not in seen_models:
|
||||
seen_models.add(m["name"])
|
||||
unique_models.append(m)
|
||||
models = sorted(unique_models, key=lambda m: m["name"])
|
||||
|
||||
# Backend-specific directories
|
||||
controller_dirs = find_dirs(root, CONTROLLER_DIR_PATTERNS)
|
||||
model_dirs = find_dirs(root, MODEL_DIR_PATTERNS)
|
||||
dto_dirs = find_dirs(root, DTO_DIR_PATTERNS)
|
||||
|
||||
# 8. Summary
|
||||
mock_count = sum(1 for a in apis if a.get("mock_detected"))
|
||||
real_count = sum(1 for a in apis if a.get("integrated"))
|
||||
backend_routes = [r for r in routes if r.get("type") == "backend"]
|
||||
frontend_routes = [r for r in routes if r.get("type") != "backend"]
|
||||
|
||||
analysis = {
|
||||
"project": {
|
||||
"root": str(root),
|
||||
"name": framework_info.get("name", root.name),
|
||||
"framework": framework_info["framework"],
|
||||
"detected_frameworks": framework_info.get("detected_frameworks", []),
|
||||
"key_dependencies": framework_info.get("key_deps", {}),
|
||||
"stack_type": "backend" if fw in ("django", "fastapi", "flask", "nestjs", "express", "fastify") and not frontend_routes else
|
||||
"fullstack" if backend_routes and frontend_routes else "frontend",
|
||||
},
|
||||
"structure": {
|
||||
"total_files": len(all_files),
|
||||
"components": component_counts,
|
||||
"route_dirs": [str(d) for d in route_dirs],
|
||||
"api_dirs": [str(d) for d in api_dirs],
|
||||
"state_dirs": [str(d) for d in state_dirs],
|
||||
"i18n_dirs": [str(d) for d in i18n_dirs],
|
||||
"controller_dirs": [str(d) for d in controller_dirs],
|
||||
"model_dirs": [str(d) for d in model_dirs],
|
||||
"dto_dirs": [str(d) for d in dto_dirs],
|
||||
},
|
||||
"routes": {
|
||||
"count": len(routes),
|
||||
"frontend_pages": frontend_routes,
|
||||
"backend_endpoints": backend_routes,
|
||||
"pages": routes, # backward compat
|
||||
},
|
||||
"apis": {
|
||||
"total": len(apis),
|
||||
"integrated": real_count,
|
||||
"mock": mock_count,
|
||||
"endpoints": apis,
|
||||
},
|
||||
"enums": {
|
||||
"count": len(enums),
|
||||
"definitions": enums,
|
||||
},
|
||||
"models": {
|
||||
"count": len(models),
|
||||
"definitions": models,
|
||||
},
|
||||
"summary": {
|
||||
"pages": len(frontend_routes),
|
||||
"backend_endpoints": len(backend_routes),
|
||||
"api_endpoints": len(apis),
|
||||
"api_integrated": real_count,
|
||||
"api_mock": mock_count,
|
||||
"enums": len(enums),
|
||||
"models": len(models),
|
||||
"has_i18n": len(i18n_dirs) > 0,
|
||||
"has_state_management": len(state_dirs) > 0,
|
||||
"stack_type": "backend" if fw in ("django", "fastapi", "flask", "nestjs", "express", "fastify") and not frontend_routes else
|
||||
"fullstack" if backend_routes and frontend_routes else "frontend",
|
||||
},
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
|
||||
def format_markdown(analysis: Dict[str, Any]) -> str:
|
||||
"""Format analysis as markdown summary."""
|
||||
lines = []
|
||||
proj = analysis["project"]
|
||||
summary = analysis["summary"]
|
||||
stack = summary.get("stack_type", "frontend")
|
||||
|
||||
lines.append(f"# Codebase Analysis: {proj['name'] or 'Project'}")
|
||||
lines.append("")
|
||||
lines.append(f"**Framework:** {proj['framework']}")
|
||||
lines.append(f"**Stack type:** {stack}")
|
||||
lines.append(f"**Total files:** {analysis['structure']['total_files']}")
|
||||
if summary.get("pages"):
|
||||
lines.append(f"**Frontend pages:** {summary['pages']}")
|
||||
if summary.get("backend_endpoints"):
|
||||
lines.append(f"**Backend endpoints:** {summary['backend_endpoints']}")
|
||||
lines.append(f"**API calls detected:** {summary['api_endpoints']} "
|
||||
f"({summary['api_integrated']} integrated, {summary['api_mock']} mock)")
|
||||
lines.append(f"**Enums:** {summary['enums']}")
|
||||
if summary.get("models"):
|
||||
lines.append(f"**Models/entities:** {summary['models']}")
|
||||
lines.append(f"**i18n:** {'Yes' if summary['has_i18n'] else 'No'}")
|
||||
lines.append(f"**State management:** {'Yes' if summary['has_state_management'] else 'No'}")
|
||||
lines.append("")
|
||||
|
||||
if analysis["routes"]["pages"]:
|
||||
lines.append("## Pages / Routes")
|
||||
lines.append("")
|
||||
lines.append("| # | Route | Source |")
|
||||
lines.append("|---|-------|--------|")
|
||||
for i, r in enumerate(analysis["routes"]["pages"], 1):
|
||||
src = r.get("source", "").split("/")[-1]
|
||||
fs = " (fs)" if r.get("filesystem") else ""
|
||||
lines.append(f"| {i} | `{r['path']}` | {src}{fs} |")
|
||||
lines.append("")
|
||||
|
||||
if analysis["apis"]["endpoints"]:
|
||||
lines.append("## API Endpoints")
|
||||
lines.append("")
|
||||
lines.append("| Method | Path | Integrated | Source |")
|
||||
lines.append("|--------|------|-----------|--------|")
|
||||
for a in analysis["apis"]["endpoints"]:
|
||||
src = a.get("source", "").split("/")[-1]
|
||||
status = "✅" if a.get("integrated") else "⚠️ Mock"
|
||||
lines.append(f"| {a['method']} | `{a['path']}` | {status} | {src} |")
|
||||
lines.append("")
|
||||
|
||||
if analysis["enums"]["definitions"]:
|
||||
lines.append("## Enums & Constants")
|
||||
lines.append("")
|
||||
for e in analysis["enums"]["definitions"]:
|
||||
lines.append(f"### {e['name']} ({e['type']})")
|
||||
if e["values"]:
|
||||
lines.append("| Key | Value |")
|
||||
lines.append("|-----|-------|")
|
||||
for k, v in e["values"].items():
|
||||
lines.append(f"| {k} | {v} |")
|
||||
lines.append("")
|
||||
|
||||
if analysis.get("models", {}).get("definitions"):
|
||||
lines.append("## Models / Entities")
|
||||
lines.append("")
|
||||
for m in analysis["models"]["definitions"]:
|
||||
lines.append(f"### {m['name']} ({m.get('framework', '')})")
|
||||
if m.get("fields"):
|
||||
lines.append("| Field | Type | Args |")
|
||||
lines.append("|-------|------|------|")
|
||||
for fld in m["fields"]:
|
||||
lines.append(f"| {fld['name']} | {fld['type']} | {fld.get('args', '')} |")
|
||||
lines.append("")
|
||||
|
||||
if proj.get("key_dependencies"):
|
||||
lines.append("## Key Dependencies")
|
||||
lines.append("")
|
||||
for dep, ver in sorted(proj["key_dependencies"].items()):
|
||||
lines.append(f"- `{dep}`: {ver}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze any codebase (frontend, backend, fullstack) for PRD generation"
|
||||
)
|
||||
parser.add_argument("project", help="Path to project root")
|
||||
parser.add_argument("-o", "--output", help="Output file (default: stdout)")
|
||||
parser.add_argument(
|
||||
"-f", "--format",
|
||||
choices=["json", "markdown"],
|
||||
default="json",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
analysis = analyze_project(Path(args.project))
|
||||
|
||||
if args.format == "markdown":
|
||||
output = format_markdown(analysis)
|
||||
else:
|
||||
output = json.dumps(analysis, indent=2, ensure_ascii=False)
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(output)
|
||||
print(f"Written to {args.output}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
435
product-team/code-to-prd/scripts/prd_scaffolder.py
Executable file
435
product-team/code-to-prd/scripts/prd_scaffolder.py
Executable file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scaffold PRD directory structure from frontend_analyzer.py output.
|
||||
|
||||
Reads analysis JSON and creates the prd/ directory with README.md,
|
||||
per-page stubs, and appendix files pre-populated with extracted data.
|
||||
|
||||
Stdlib only — no third-party dependencies.
|
||||
|
||||
Usage:
|
||||
python3 frontend_analyzer.py /path/to/project -o analysis.json
|
||||
python3 prd_scaffolder.py analysis.json
|
||||
python3 prd_scaffolder.py analysis.json --output-dir ./prd --project-name "My App"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
"""Convert text to a filename-safe slug."""
|
||||
text = text.strip().lower()
|
||||
text = re.sub(r"[/:{}*?\"<>|]", "-", text)
|
||||
text = re.sub(r"[^a-z0-9\-]", "-", text)
|
||||
text = re.sub(r"-+", "-", text)
|
||||
return text.strip("-")
|
||||
|
||||
|
||||
def route_to_page_name(route: str) -> str:
|
||||
"""Convert a route path to a human-readable page name."""
|
||||
if route == "/" or route == "":
|
||||
return "Home"
|
||||
parts = route.strip("/").split("/")
|
||||
# Remove dynamic segments for naming
|
||||
clean = [p for p in parts if not p.startswith(":") and not p.startswith("*")]
|
||||
if not clean:
|
||||
clean = [p.lstrip(":*") for p in parts]
|
||||
return " ".join(w.capitalize() for w in "-".join(clean).replace("_", "-").split("-"))
|
||||
|
||||
|
||||
def generate_readme(project_name: str, routes: List[Dict], summary: Dict, date: str) -> str:
|
||||
"""Generate the PRD README.md."""
|
||||
lines = [
|
||||
f"# {project_name} — Product Requirements Document",
|
||||
"",
|
||||
f"> Generated: {date}",
|
||||
"",
|
||||
"## System Overview",
|
||||
"",
|
||||
f"<!-- TODO: Describe what {project_name} does, its business context, and primary users -->",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
f"| Metric | Count |",
|
||||
f"|--------|-------|",
|
||||
f"| Pages | {summary.get('pages', 0)} |",
|
||||
f"| API Endpoints | {summary.get('api_endpoints', 0)} |",
|
||||
f"| Integrated APIs | {summary.get('api_integrated', 0)} |",
|
||||
f"| Mock APIs | {summary.get('api_mock', 0)} |",
|
||||
f"| Enums/Constants | {summary.get('enums', 0)} |",
|
||||
f"| i18n | {'Yes' if summary.get('has_i18n') else 'No'} |",
|
||||
f"| State Management | {'Yes' if summary.get('has_state_management') else 'No'} |",
|
||||
"",
|
||||
"## Module Overview",
|
||||
"",
|
||||
"| Module | Pages | Core Functionality |",
|
||||
"|--------|-------|--------------------|",
|
||||
"| <!-- TODO: Group pages into modules --> | | |",
|
||||
"",
|
||||
"## Page Inventory",
|
||||
"",
|
||||
"| # | Page Name | Route | Module | Doc Link |",
|
||||
"|---|-----------|-------|--------|----------|",
|
||||
]
|
||||
|
||||
for i, route in enumerate(routes, 1):
|
||||
path = route.get("path", "/")
|
||||
name = route_to_page_name(path)
|
||||
slug = slugify(name) or f"page-{i}"
|
||||
filename = f"{i:02d}-{slug}.md"
|
||||
lines.append(f"| {i} | {name} | `{path}` | <!-- TODO --> | [→](./pages/{filename}) |")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Global Notes",
|
||||
"",
|
||||
"### Permission Model",
|
||||
"<!-- TODO: Summarize auth/role system if present -->",
|
||||
"",
|
||||
"### Common Interaction Patterns",
|
||||
"<!-- TODO: Global rules — delete confirmations, default sort, etc. -->",
|
||||
"",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_page_stub(route: Dict, index: int, date: str) -> str:
|
||||
"""Generate a per-page PRD stub."""
|
||||
path = route.get("path", "/")
|
||||
name = route_to_page_name(path)
|
||||
source = route.get("source", "unknown")
|
||||
|
||||
return f"""# {name}
|
||||
|
||||
> **Route:** `{path}`
|
||||
> **Module:** <!-- TODO -->
|
||||
> **Source:** `{source}`
|
||||
> **Generated:** {date}
|
||||
|
||||
## Overview
|
||||
<!-- TODO: 2-3 sentences — core function and use case -->
|
||||
|
||||
## Layout
|
||||
<!-- TODO: Region breakdown — search area, table, detail panel, action bar, etc. -->
|
||||
|
||||
## Fields
|
||||
|
||||
### Search / Filters
|
||||
| Field | Type | Required | Options / Enum | Default | Notes |
|
||||
|-------|------|----------|---------------|---------|-------|
|
||||
| <!-- TODO --> | | | | | |
|
||||
|
||||
### Data Table
|
||||
| Column | Format | Sortable | Filterable | Notes |
|
||||
|--------|--------|----------|-----------|-------|
|
||||
| <!-- TODO --> | | | | |
|
||||
|
||||
### Actions
|
||||
| Button | Visibility Condition | Behavior |
|
||||
|--------|---------------------|----------|
|
||||
| <!-- TODO --> | | |
|
||||
|
||||
## Interactions
|
||||
|
||||
### Page Load
|
||||
<!-- TODO: What happens on mount — default queries, preloaded data -->
|
||||
|
||||
### Search
|
||||
- **Trigger:** <!-- TODO -->
|
||||
- **Behavior:** <!-- TODO -->
|
||||
- **Special rules:** <!-- TODO -->
|
||||
|
||||
### Create / Edit
|
||||
- **Trigger:** <!-- TODO -->
|
||||
- **Modal/drawer content:** <!-- TODO -->
|
||||
- **Validation:** <!-- TODO -->
|
||||
- **On success:** <!-- TODO -->
|
||||
|
||||
### Delete
|
||||
- **Trigger:** <!-- TODO -->
|
||||
- **Confirmation:** <!-- TODO -->
|
||||
- **On success:** <!-- TODO -->
|
||||
|
||||
## API Dependencies
|
||||
|
||||
| API | Method | Path | Trigger | Integrated | Notes |
|
||||
|-----|--------|------|---------|-----------|-------|
|
||||
| <!-- TODO --> | | | | | |
|
||||
|
||||
## Page Relationships
|
||||
- **From:** <!-- TODO: Source pages + params -->
|
||||
- **To:** <!-- TODO: Target pages + params -->
|
||||
- **Data coupling:** <!-- TODO: Cross-page refresh triggers -->
|
||||
|
||||
## Business Rules
|
||||
<!-- TODO: Anything that doesn't fit above -->
|
||||
"""
|
||||
|
||||
|
||||
def generate_enum_dictionary(enums: List[Dict]) -> str:
|
||||
"""Generate the enum dictionary appendix."""
|
||||
lines = [
|
||||
"# Enum & Constant Dictionary",
|
||||
"",
|
||||
"All enums, status codes, and type mappings extracted from the codebase.",
|
||||
"",
|
||||
]
|
||||
|
||||
if not enums:
|
||||
lines.append("*No enums detected. Manual review recommended.*")
|
||||
return "\n".join(lines)
|
||||
|
||||
for e in enums:
|
||||
lines.append(f"## {e['name']}")
|
||||
lines.append(f"**Type:** {e.get('type', 'unknown')} | **Source:** `{e.get('source', 'unknown').split('/')[-1]}`")
|
||||
lines.append("")
|
||||
if e.get("values"):
|
||||
lines.append("| Key | Value |")
|
||||
lines.append("|-----|-------|")
|
||||
for k, v in e["values"].items():
|
||||
lines.append(f"| `{k}` | {v} |")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_api_inventory(apis: List[Dict]) -> str:
|
||||
"""Generate the API inventory appendix."""
|
||||
lines = [
|
||||
"# API Inventory",
|
||||
"",
|
||||
"All API endpoints detected in the codebase.",
|
||||
"",
|
||||
]
|
||||
|
||||
if not apis:
|
||||
lines.append("*No API calls detected. Manual review recommended.*")
|
||||
return "\n".join(lines)
|
||||
|
||||
integrated = [a for a in apis if a.get("integrated")]
|
||||
mocked = [a for a in apis if a.get("mock_detected") and not a.get("integrated")]
|
||||
unknown = [a for a in apis if not a.get("integrated") and not a.get("mock_detected")]
|
||||
|
||||
for label, group in [("Integrated APIs", integrated), ("Mock / Stub APIs", mocked), ("Unknown Status", unknown)]:
|
||||
if group:
|
||||
lines.append(f"## {label}")
|
||||
lines.append("")
|
||||
lines.append("| Method | Path | Source | Notes |")
|
||||
lines.append("|--------|------|--------|-------|")
|
||||
for a in group:
|
||||
src = a.get("source", "").split("/")[-1]
|
||||
lines.append(f"| {a.get('method', '?')} | `{a.get('path', '?')}` | {src} | |")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_page_relationships(routes: List[Dict]) -> str:
|
||||
"""Generate page relationships appendix stub."""
|
||||
lines = [
|
||||
"# Page Relationships",
|
||||
"",
|
||||
"Navigation flow and data coupling between pages.",
|
||||
"",
|
||||
"## Navigation Map",
|
||||
"",
|
||||
"<!-- TODO: Fill in after page-by-page analysis -->",
|
||||
"",
|
||||
"```",
|
||||
"Home",
|
||||
]
|
||||
|
||||
for r in routes[:20]: # Cap at 20 for readability
|
||||
name = route_to_page_name(r.get("path", "/"))
|
||||
lines.append(f" ├── {name}")
|
||||
|
||||
if len(routes) > 20:
|
||||
lines.append(f" └── ... ({len(routes) - 20} more)")
|
||||
|
||||
lines.extend([
|
||||
"```",
|
||||
"",
|
||||
"## Cross-Page Data Dependencies",
|
||||
"",
|
||||
"| Source Page | Target Page | Trigger | Data Passed |",
|
||||
"|-----------|------------|---------|------------|",
|
||||
"| <!-- TODO --> | | | |",
|
||||
"",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def scaffold(analysis: Dict[str, Any], output_dir: Path, project_name: Optional[str] = None):
|
||||
"""Create the full PRD directory structure."""
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
name = project_name or analysis.get("project", {}).get("name", "Project")
|
||||
routes = analysis.get("routes", {}).get("pages", [])
|
||||
apis = analysis.get("apis", {}).get("endpoints", [])
|
||||
enums = analysis.get("enums", {}).get("definitions", [])
|
||||
summary = analysis.get("summary", {})
|
||||
|
||||
# Create directories
|
||||
pages_dir = output_dir / "pages"
|
||||
appendix_dir = output_dir / "appendix"
|
||||
pages_dir.mkdir(parents=True, exist_ok=True)
|
||||
appendix_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# README.md
|
||||
readme = generate_readme(name, routes, summary, date)
|
||||
(output_dir / "README.md").write_text(readme)
|
||||
print(f" Created: README.md")
|
||||
|
||||
# Per-page stubs
|
||||
for i, route in enumerate(routes, 1):
|
||||
page_name = route_to_page_name(route.get("path", "/"))
|
||||
slug = slugify(page_name) or f"page-{i}"
|
||||
filename = f"{i:02d}-{slug}.md"
|
||||
content = generate_page_stub(route, i, date)
|
||||
(pages_dir / filename).write_text(content)
|
||||
print(f" Created: pages/{filename}")
|
||||
|
||||
# Appendix
|
||||
(appendix_dir / "enum-dictionary.md").write_text(generate_enum_dictionary(enums))
|
||||
print(f" Created: appendix/enum-dictionary.md")
|
||||
|
||||
(appendix_dir / "api-inventory.md").write_text(generate_api_inventory(apis))
|
||||
print(f" Created: appendix/api-inventory.md")
|
||||
|
||||
(appendix_dir / "page-relationships.md").write_text(generate_page_relationships(routes))
|
||||
print(f" Created: appendix/page-relationships.md")
|
||||
|
||||
print(f"\n✅ PRD scaffold complete: {output_dir}")
|
||||
print(f" {len(routes)} page stubs, {len(apis)} API endpoints, {len(enums)} enums")
|
||||
print(f"\n Next: Review each page stub and fill in the TODO sections.")
|
||||
|
||||
|
||||
def validate_analysis(analysis: Dict[str, Any]) -> List[str]:
|
||||
"""Validate analysis JSON has the required structure. Returns list of errors."""
|
||||
errors = []
|
||||
|
||||
if not isinstance(analysis, dict):
|
||||
return ["Analysis must be a JSON object"]
|
||||
|
||||
if "error" in analysis:
|
||||
errors.append(f"Analysis contains error: {analysis['error']}")
|
||||
|
||||
required_keys = ["project", "routes", "apis"]
|
||||
for key in required_keys:
|
||||
if key not in analysis:
|
||||
errors.append(f"Missing required key: '{key}'")
|
||||
|
||||
if "project" in analysis:
|
||||
proj = analysis["project"]
|
||||
if not isinstance(proj, dict):
|
||||
errors.append("'project' must be an object")
|
||||
elif "framework" not in proj:
|
||||
errors.append("'project.framework' is missing")
|
||||
|
||||
if "routes" in analysis:
|
||||
routes = analysis["routes"]
|
||||
if not isinstance(routes, dict):
|
||||
errors.append("'routes' must be an object")
|
||||
elif "pages" not in routes and "frontend_pages" not in routes and "backend_endpoints" not in routes:
|
||||
errors.append("'routes' must contain 'pages', 'frontend_pages', or 'backend_endpoints'")
|
||||
|
||||
if "apis" in analysis:
|
||||
apis = analysis["apis"]
|
||||
if not isinstance(apis, dict):
|
||||
errors.append("'apis' must be an object")
|
||||
elif "endpoints" not in apis:
|
||||
errors.append("'apis.endpoints' is missing")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def print_summary(output_dir: Path, analysis: Dict[str, Any]):
|
||||
"""Print a structured summary of what was generated."""
|
||||
routes = analysis.get("routes", {}).get("pages", [])
|
||||
apis = analysis.get("apis", {}).get("endpoints", [])
|
||||
enums = analysis.get("enums", {}).get("definitions", [])
|
||||
models = analysis.get("models", {}).get("definitions", [])
|
||||
summary = analysis.get("summary", {})
|
||||
stack = summary.get("stack_type", "unknown")
|
||||
|
||||
print(f"\nPRD scaffold complete: {output_dir}/")
|
||||
print(f" Stack type: {stack}")
|
||||
print(f" Page stubs: {len(routes)}")
|
||||
print(f" API endpoints: {len(apis)}")
|
||||
print(f" Enums: {len(enums)}")
|
||||
if models:
|
||||
print(f" Models: {len(models)}")
|
||||
print(f"\n Next: Review each page stub and fill in the TODO sections.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Scaffold PRD directory from codebase analysis"
|
||||
)
|
||||
parser.add_argument("analysis", help="Path to analysis JSON from codebase_analyzer.py")
|
||||
parser.add_argument("-o", "--output-dir", default="prd", help="Output directory (default: prd/)")
|
||||
parser.add_argument("-n", "--project-name", help="Override project name")
|
||||
parser.add_argument("--validate-only", action="store_true",
|
||||
help="Validate analysis JSON without generating files")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Show what would be created without writing files")
|
||||
args = parser.parse_args()
|
||||
|
||||
analysis_path = Path(args.analysis)
|
||||
if not analysis_path.exists():
|
||||
print(f"Error: Analysis file not found: {analysis_path}")
|
||||
raise SystemExit(2)
|
||||
|
||||
try:
|
||||
with open(analysis_path) as f:
|
||||
analysis = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON in {analysis_path}: {e}")
|
||||
raise SystemExit(2)
|
||||
|
||||
# Validate
|
||||
errors = validate_analysis(analysis)
|
||||
if errors:
|
||||
print(f"Validation errors in {analysis_path}:")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.validate_only:
|
||||
print(f"Analysis file is valid: {analysis_path}")
|
||||
routes = analysis.get("routes", {}).get("pages", [])
|
||||
print(f" {len(routes)} routes, "
|
||||
f"{len(analysis.get('apis', {}).get('endpoints', []))} APIs, "
|
||||
f"{len(analysis.get('enums', {}).get('definitions', []))} enums")
|
||||
return
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
|
||||
if args.dry_run:
|
||||
routes = analysis.get("routes", {}).get("pages", [])
|
||||
print(f"Dry run — would create in {output_dir}/:\n")
|
||||
print(f" {output_dir}/README.md")
|
||||
for i, route in enumerate(routes, 1):
|
||||
name = route_to_page_name(route.get("path", "/"))
|
||||
slug = slugify(name) or f"page-{i}"
|
||||
print(f" {output_dir}/pages/{i:02d}-{slug}.md")
|
||||
print(f" {output_dir}/appendix/enum-dictionary.md")
|
||||
print(f" {output_dir}/appendix/api-inventory.md")
|
||||
print(f" {output_dir}/appendix/page-relationships.md")
|
||||
print(f"\n Total: {len(routes) + 4} files")
|
||||
return
|
||||
|
||||
print(f"Scaffolding PRD in {output_dir}/...\n")
|
||||
scaffold(analysis, output_dir, args.project_name)
|
||||
print_summary(output_dir, analysis)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
product-team/code-to-prd/settings.json
Normal file
15
product-team/code-to-prd/settings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "code-to-prd",
|
||||
"displayName": "Code → PRD",
|
||||
"version": "2.1.2",
|
||||
"description": "Reverse-engineer any codebase into a complete PRD. Analyzes routes, components, models, APIs, and interactions. Frontend (React, Vue, Next.js), backend (NestJS, Django, FastAPI), and fullstack.",
|
||||
"author": "Alireza Rezvani",
|
||||
"license": "MIT",
|
||||
"platforms": ["claude-code", "openclaw", "codex"],
|
||||
"category": "product",
|
||||
"tags": ["prd", "product-requirements", "reverse-engineering", "frontend", "backend", "fullstack", "documentation", "code-analysis", "react", "vue", "angular", "next-js", "nestjs", "django", "fastapi", "express"],
|
||||
"repository": "https://github.com/alirezarezvani/claude-skills",
|
||||
"commands": {
|
||||
"code-to-prd": "/code-to-prd"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user