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:
Alireza Rezvani
2026-03-17 13:24:30 +01:00
committed by GitHub
15 changed files with 2383 additions and 14 deletions

View File

@@ -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
View 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`

View 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": "./"
}

View 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

View 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).

View 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"
}
}

View File

@@ -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 |

View File

@@ -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

View File

@@ -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

View 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
}
```

View 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 |

View File

@@ -0,0 +1 @@
prd/

View 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()

View 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()

View 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"
}
}