Files
16gagent/scripts/architecture-agent.mjs
T
2026-06-06 10:40:48 +08:00

1221 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Architecture Agent — SF-02
*
* 根据 SF-01 生成的 PRD 自动生成技术架构。
*
* 核心功能:
* - 技术选型:前端/后端/数据库/部署
* - 系统架构图:ASCII art 三层架构
* - 数据流:请求链路文档
* - 模块划分:按功能域拆解
* - 数据库设计:表结构 + 字段 + 关系
* - API 设计:按资源分组
* - 目录结构:完整 /src 树
*
* Usage:
* node scripts/architecture-agent.mjs --input <prd.json> [options]
* node scripts/architecture-agent.mjs --input-text "做一个宠物管理 App" [options]
*
* Options:
* --input <path> PRD JSON 文件路径
* --input-text <text> 直接传入需求(先调 SF-01 生成 PRD)
* --output <path> 输出架构 JSON 文件路径
* --pretty 格式化 JSON 输出
* --verbose 详细输出
* --help 显示帮助
*
* @module architecture-agent
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createContract } from "./model-contract.mjs";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
// ═══════════════════════════════════════════════════════
// 1. Tech Stack Templates
// ═══════════════════════════════════════════════════════
/**
* Tech stack options keyed by platform combination.
*/
const TECH_STACKS = {
"wechat-miniapp": {
frontend: "微信小程序原生 / Taro 3.x + React",
backend: "Node.js + Express / 云函数(微信云开发)",
database: "MySQL 8.0(用户数据)+ 微信云数据库(小程序数据)",
deployment: "微信云托管 / 腾讯云 Lighthouse",
},
miniapp: {
frontend: "uni-app 3.x(多端小程序)",
backend: "Node.js + Koa2 / NestJS",
database: "PostgreSQL 15 + Redis 7",
deployment: "阿里云 ECS / Docker + Nginx",
},
"ios-only": {
frontend: "SwiftUI + CombineiOS 17+",
backend: "Vapor 4Swift 服务端)/ Node.js + Express",
database: "PostgreSQL 15 + Redis 7",
deployment: "AWS EC2 / Railway + App Store Connect",
},
"android-only": {
frontend: "Jetpack Compose + Kotlin Coroutines",
backend: "Spring Boot 3 / Ktor",
database: "PostgreSQL 15 + Redis 7",
deployment: "GCP Cloud Run / AWS ECS",
},
"web-only": {
frontend: "React 19 + TypeScript + Vite",
backend: "NestJS / Fastify",
database: "PostgreSQL 15 + Redis 7",
deployment: "Docker + Nginx / Vercel + Railway",
},
"mobile-web": {
frontend: "React Native 0.76 + Expo / Flutter 3.x",
backend: "NestJS + Prisma",
database: "PostgreSQL 15 + Redis 7 + MinIO(文件存储)",
deployment: "Docker Compose + Nginx / K8s(规模化)",
},
mobile: {
frontend: "React Native 0.76 + Expo",
backend: "NestJS + Prisma",
database: "PostgreSQL 15 + Redis 7",
deployment: "Docker Compose + Nginx",
},
default: {
frontend: "React 19 + TypeScript + Vite",
backend: "NestJS + Prisma",
database: "PostgreSQL 15 + Redis 7",
deployment: "Docker + Nginx",
},
};
// ═══════════════════════════════════════════════════════
// 2. Architecture Diagram Generator (ASCII)
// ═══════════════════════════════════════════════════════
/**
* Generate an ASCII architecture diagram.
*
* @param {string} projectName
* @param {object} stack — Tech stack
* @param {string[]} modules — Module names
* @returns {string}
*/
export function generateArchDiagram(projectName, stack, modules) {
const modList = modules.slice(0, 6).map((m, i) => `${String(i + 1).padEnd(3)} ${m.padEnd(24)}`).join("\n");
return [
`┌─────────────────────────────────────────────────────────┐`,
`${projectName} — System Architecture │`,
`├─────────────────────────────────────────────────────────┤`,
`│ │`,
`│ ┌──────────────────────┐ ┌──────────────────────┐ │`,
`│ │ Client Layer │ │ Admin / Web │ │`,
`│ │ ${(stack.frontend || "Mobile App").padEnd(20)} │ │ React + Vite (Web) │ │`,
`│ └──────────┬───────────┘ └──────────┬───────────┘ │`,
`│ │ │ │`,
`│ └──────────┬────────────────┘ │`,
`│ │ │`,
`│ ┌─────────▼──────────┐ │`,
`│ │ API Gateway │ │`,
`│ │ Nginx / Kong │ │`,
`│ └─────────┬──────────┘ │`,
`│ │ │`,
`│ ┌─────────▼──────────┐ │`,
`│ │ Backend Services │ │`,
`│ │ ${(stack.backend || "NestJS").padEnd(20)} │ │`,
`│ └─────────┬──────────┘ │`,
`│ │ │`,
`│ ┌───────────────┼───────────────┐ │`,
`│ │ │ │ │`,
`│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │`,
`│ │ ${(stack.database || "PostgreSQL").split("+")[0].trim().padEnd(12)}│ │ Redis Cache │ │ MinIO │ │`,
`│ │ Primary │ │ Session │ │ Files │ │`,
`│ └───────────┘ └─────────────┘ └──────────┘ │`,
`│ │`,
`├─────────────────────────────────────────────────────────┤`,
`│ Modules: │`,
`${modList}`,
`│ │`,
`└─────────────────────────────────────────────────────────┘`,
].join("\n");
}
// ═══════════════════════════════════════════════════════
// 3. Data Flow Generator
// ═══════════════════════════════════════════════════════
/**
* Generate data flow documentation from pages and API requirements.
*
* @param {object[]} pages
* @param {object[]} apis
* @returns {object[]}
*/
export function generateDataFlows(pages, apis) {
const flows = [];
for (const page of pages.slice(0, 5)) {
const pageName = page.name;
const route = page.route;
// Find APIs related to this page (simple heuristic: route name in API path)
const relatedApis = apis.filter(a =>
route.replace(/[:/]/g, "").split("/").some(seg =>
a.path.includes(seg.replace(":id", ""))
)
).slice(0, 2);
if (relatedApis.length > 0) {
flows.push({
page: pageName,
route,
description: page.description,
dataFlow: relatedApis.map(a => `${a.method} ${a.path}${a.description}`),
direction: "Client → API Gateway → Backend → Database → Response → Client",
});
}
}
// If no specific flows found, generate generic ones from APIs
if (flows.length === 0 && apis.length > 0) {
flows.push({
page: "通用页面",
route: "/*",
description: "通用数据流",
dataFlow: apis.slice(0, 4).map(a => `${a.method} ${a.path}${a.description}`),
direction: "Client → API Gateway → Backend → Database → Response → Client",
});
}
return flows;
}
// ═══════════════════════════════════════════════════════
// 4. Module Decomposition Engine
// ═══════════════════════════════════════════════════════
/**
* Decompose features into backend modules.
*
* @param {object[]} features
* @param {string} domain
* @returns {object[]}
*/
export function decomposeModules(features, domain) {
// Group features into logical modules by keyword
const moduleMap = new Map();
const modulePatterns = {
auth: ["用户", "登录", "注册", "权限", "角色", "auth", "profile"],
settings: ["设置", "偏好", "配置"],
notification: ["通知", "提醒", "推送", "消息"],
storage: ["上传", "文件", "图片", "相册", "照片"],
analytics: ["统计", "报告", "分析", "趋势"],
content: ["社区", "分享", "动态", "评论", "评价"],
payment: ["支付", "订单", "购物车", "优惠券"],
schedule: ["日程", "日历", "提醒", "预约"],
workflow: ["审批", "流程", "考勤", "请假", "报销"],
};
// Default grouping: 1 feature = 1 module
for (const feature of features) {
let assigned = false;
for (const [modKey, keywords] of Object.entries(modulePatterns)) {
if (keywords.some(kw => feature.name.includes(kw))) {
if (!moduleMap.has(modKey)) {
moduleMap.set(modKey, { name: modKey, label: modKey.charAt(0).toUpperCase() + modKey.slice(1), features: [] });
}
moduleMap.get(modKey).features.push(feature.name);
assigned = true;
break;
}
}
if (!assigned) {
const key = feature.name;
if (!moduleMap.has(key)) {
moduleMap.set(key, { name: key, label: feature.name, features: [feature.name] });
}
}
}
// Domain-specific module additions
const domainModules = {
pet: [{ name: "pet", label: "宠物管理", features: ["宠物档案", "健康日程"] }],
ecommerce: [
{ name: "product", label: "商品管理", features: ["商品浏览"] },
{ name: "order", label: "订单管理", features: ["订单管理"] },
],
education: [
{ name: "course", label: "课程管理", features: ["课程中心", "视频播放"] },
{ name: "exercise", label: "题库管理", features: ["题库练习"] },
],
enterprise: [
{ name: "approval", label: "审批引擎", features: ["审批流程"] },
{ name: "attendance", label: "考勤管理", features: ["考勤打卡"] },
{ name: "department", label: "组织架构", features: ["部门管理"] },
],
fitness: [
{ name: "checkin", label: "打卡引擎", features: ["打卡记录"] },
{ name: "plan", label: "训练计划", features: ["训练计划"] },
],
note: [
{ name: "editor", label: "编辑器", features: ["笔记编辑"] },
{ name: "search", label: "搜索引擎", features: ["全文搜索"] },
],
};
const extraModules = domainModules[domain] || [];
for (const em of extraModules) {
if (!moduleMap.has(em.name)) {
moduleMap.set(em.name, em);
}
}
return Array.from(moduleMap.values());
}
// ═══════════════════════════════════════════════════════
// 5. Database Design Engine
// ═══════════════════════════════════════════════════════
/**
* Generate database schema from PRD.
*
* @param {object[]} features
* @param {object[]} apis
* @param {string} domain
* @returns {object[]} — Array of table definitions
*/
/**
* Chinese-to-English slug mapping for common entity names.
*/
const ZH_SLUG_MAP = {
"书籍库": "books", "书籍": "books", "书评": "reviews", "阅读状态": "reading_status", "阅读进度": "reading_progress",
"笔记": "notes", "标注": "annotations", "标签": "tags", "统计": "stats", "仪表盘": "dashboard",
"看板": "boards", "任务": "tasks", "成员": "members", "活动日志": "activity_logs", "日志": "logs",
"食物库": "foods", "食物": "foods", "客户": "clients", "餐计划": "meal_plans", "营养": "nutrition",
"购物清单": "shopping_lists", "模板": "templates", "商品": "products", "订单": "orders",
"库存": "inventory", "供应商": "suppliers", "预警": "alerts", "操作日志": "operation_logs",
"工单": "tickets", "审批": "approvals", "考勤": "attendance", "员工": "employees",
"课程": "courses", "章节": "chapters", "学员": "students", "作业": "assignments",
"预约": "appointments", "服务": "services", "宠物": "pets", "日程": "schedules",
"记录": "records", "相册": "albums", "医院": "hospitals", "文章": "articles",
"评论": "comments", "媒体": "media", "项目": "projects", "资产": "assets",
"公告": "notices", "通知": "notifications", "报销": "expenses", "请假": "leaves",
"打卡": "checkins", "部门": "departments", "薪资": "salaries", "绩效": "performance",
"客户管理": "customers", "销售漏斗": "sales_pipeline", "跟进记录": "follow_ups",
// v1.1: Contract / Inspection / Callback / Warehouse / Schedule
"合同": "contracts", "合同创建": "contracts", "合同归档": "contracts", "到期提醒": "reminders",
"巡检": "inspections", "巡检计划": "inspection_plans", "巡检记录": "inspection_records",
"故障": "faults", "故障上报": "faults", "设备": "equipment", "设备台账": "equipment",
"回访": "callbacks", "回访计划": "callback_plans", "回访记录": "callback_records",
"满意度": "satisfaction", "满意度评价": "satisfaction",
"入库": "stock_in", "入库登记": "stock_in", "出库": "stock_out", "出库审批": "stock_out",
"盘点": "stocktaking", "库存盘点": "stocktaking", "库存预警": "stock_alerts",
"教室": "classrooms", "教室管理": "classrooms", "排课": "schedules", "排课冲突检测": "schedules",
"课表": "schedules", "课表查看": "schedules",
"合同管理": "contracts", "巡检系统": "inspections", "回访系统": "callbacks",
"出入库": "warehouse", "排课系统": "schedules",
};
function toSafeSlug(name) {
// Strip trailing Chinese/English punctuation
const cleaned = name.replace(/[。,、;:!?\.\,\;\!\?]+$/g, "").trim();
if (/^[a-z][a-z0-9_]*$/.test(cleaned)) return cleaned;
if (ZH_SLUG_MAP[cleaned]) return ZH_SLUG_MAP[cleaned];
// Longest-match-first
for (const zh of Object.keys(ZH_SLUG_MAP).sort((a,b) => b.length - a.length)) {
if (cleaned.includes(zh)) return ZH_SLUG_MAP[zh];
}
const slug = cleaned.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase().replace(/_+/g, "_").replace(/^_|_$/g, "");
return slug || "items";
}
/**
* Domain-specific field templates keyed by keyword match.
* Each entry: { match: string[], fields: [...extra fields beyond id/user_id/timestamps] }
* These are inserted BETWEEN user_id and created_at.
*/
const FIELD_TEMPLATES = [
// ── Books / Reading ──
{ match: ["book", "书籍"], fields: [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "author", type: "VARCHAR(128)" },
{ name: "isbn", type: "VARCHAR(20)" },
{ name: "cover_url", type: "TEXT" },
{ name: "publisher", type: "VARCHAR(128)" },
{ name: "publish_date", type: "DATE" },
{ name: "genre", type: "VARCHAR(64)" },
]},
{ match: ["reading_status", "阅读状态"], fields: [
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'unread', CHECK (status IN ('unread','reading','finished','abandoned'))" },
{ name: "started_at", type: "TIMESTAMPTZ" },
{ name: "finished_at", type: "TIMESTAMPTZ" },
]},
{ match: ["reading_progress", "阅读进度"], fields: [
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
{ name: "current_page", type: "INTEGER", constraints: "DEFAULT 0" },
{ name: "total_pages", type: "INTEGER" },
{ name: "percentage", type: "DECIMAL(5,2)", constraints: "DEFAULT 0" },
]},
{ match: ["review", "书评"], fields: [
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
{ name: "rating", type: "INTEGER", constraints: "CHECK (rating >= 1 AND rating <= 5)" },
{ name: "content", type: "TEXT" },
]},
// ── Tasks / Boards ──
{ match: ["task"], fields: [
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'todo'" },
{ name: "priority", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
{ name: "due_date", type: "DATE" },
{ name: "assignee_id", type: "UUID", constraints: "FK → users.id" },
]},
{ match: ["board", "看板"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "color", type: "VARCHAR(7)" },
]},
{ match: ["member", "成员"], fields: [
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
{ name: "role", type: "VARCHAR(32)", constraints: "DEFAULT 'member'" },
]},
{ match: ["activity_log", "活动日志"], fields: [
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
{ name: "action", type: "VARCHAR(64)", constraints: "NOT NULL" },
{ name: "entity_type", type: "VARCHAR(32)" },
{ name: "entity_id", type: "UUID" },
{ name: "detail", type: "JSONB" },
]},
// ── Food / Meal ──
{ match: ["food", "食物"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "category", type: "VARCHAR(64)" },
{ name: "calories_per_100g", type: "INTEGER" },
{ name: "protein_g", type: "DECIMAL(5,2)" },
{ name: "fat_g", type: "DECIMAL(5,2)" },
{ name: "carb_g", type: "DECIMAL(5,2)" },
]},
{ match: ["meal_plan", "餐计划"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "start_date", type: "DATE" },
{ name: "end_date", type: "DATE" },
{ name: "target_calories", type: "INTEGER" },
]},
{ match: ["nutrition", "营养"], fields: [
{ name: "food_id", type: "UUID", constraints: "FK → foods.id" },
{ name: "meal_plan_id", type: "UUID", constraints: "FK → meal_plans.id" },
{ name: "serving_g", type: "DECIMAL(6,2)" },
{ name: "meal_type", type: "VARCHAR(32)" },
]},
{ match: ["shopping_list", "购物清单"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "items", type: "JSONB", constraints: "DEFAULT '[]'" },
{ name: "completed", type: "BOOLEAN", constraints: "DEFAULT false" },
]},
// ── CRM ──
{ match: ["customer", "客户管理"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "email", type: "VARCHAR(256)" },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "company", type: "VARCHAR(128)" },
{ name: "source", type: "VARCHAR(64)" },
{ name: "tags", type: "JSONB", constraints: "DEFAULT '[]'" },
]},
{ match: ["sales_pipeline", "销售漏斗"], fields: [
{ name: "customer_id", type: "UUID", constraints: "FK → customers.id" },
{ name: "stage", type: "VARCHAR(32)", constraints: "NOT NULL" },
{ name: "amount", type: "DECIMAL(12,2)" },
{ name: "probability", type: "INTEGER" },
{ name: "expected_close", type: "DATE" },
]},
{ match: ["follow_up", "跟进记录"], fields: [
{ name: "customer_id", type: "UUID", constraints: "FK → customers.id" },
{ name: "type", type: "VARCHAR(32)" },
{ name: "content", type: "TEXT" },
{ name: "next_action", type: "VARCHAR(256)" },
{ name: "next_action_date", type: "DATE" },
]},
// ── Inventory ──
{ match: ["inventory", "库存"], exclude: ["stocktaking", "stock_alert", "库存盘点", "库存预警"], fields: [
{ name: "product_id", type: "UUID", constraints: "FK → products.id" },
{ name: "quantity", type: "INTEGER", constraints: "DEFAULT 0" },
{ name: "warehouse", type: "VARCHAR(64)" },
{ name: "min_stock", type: "INTEGER", constraints: "DEFAULT 0" },
{ name: "max_stock", type: "INTEGER" },
]},
{ match: ["supplier", "供应商"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "contact", type: "VARCHAR(64)" },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "email", type: "VARCHAR(256)" },
{ name: "address", type: "TEXT" },
]},
// ── Ticket / OA ──
{ match: ["ticket", "工单"], fields: [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "priority", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'open'" },
{ name: "assignee_id", type: "UUID", constraints: "FK → users.id" },
{ name: "category", type: "VARCHAR(64)" },
]},
{ match: ["employee", "员工"], fields: [
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
{ name: "email", type: "VARCHAR(256)" },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "department", type: "VARCHAR(64)" },
{ name: "position", type: "VARCHAR(64)" },
{ name: "hire_date", type: "DATE" },
{ name: "salary", type: "DECIMAL(12,2)" },
]},
// ── Education ──
{ match: ["course", "课程"], fields: [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "instructor", type: "VARCHAR(64)" },
{ name: "price", type: "DECIMAL(10,2)" },
{ name: "cover_url", type: "TEXT" },
{ name: "category", type: "VARCHAR(64)" },
]},
{ match: ["chapter", "章节"], fields: [
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "sort_order", type: "INTEGER", constraints: "DEFAULT 0" },
{ name: "duration_min", type: "INTEGER" },
]},
{ match: ["student", "学员"], fields: [
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
{ name: "email", type: "VARCHAR(256)" },
{ name: "enrolled_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
{ name: "progress", type: "DECIMAL(5,2)", constraints: "DEFAULT 0" },
]},
{ match: ["assignment", "作业"], fields: [
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "due_date", type: "DATE" },
{ name: "max_score", type: "INTEGER", constraints: "DEFAULT 100" },
]},
// ── Appointment / Service ──
{ match: ["appointment", "预约"], fields: [
{ name: "client_id", type: "UUID", constraints: "FK → clients.id" },
{ name: "service_id", type: "UUID", constraints: "FK → services.id" },
{ name: "start_time", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
{ name: "end_time", type: "TIMESTAMPTZ" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
{ name: "notes", type: "TEXT" },
]},
{ match: ["service", "服务"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "duration_min", type: "INTEGER" },
{ name: "price", type: "DECIMAL(10,2)" },
]},
// ── Asset ──
{ match: ["asset", "资产"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "category", type: "VARCHAR(64)" },
{ name: "purchase_date", type: "DATE" },
{ name: "purchase_price", type: "DECIMAL(12,2)" },
{ name: "current_value", type: "DECIMAL(12,2)" },
{ name: "location", type: "VARCHAR(128)" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'available'" },
]},
// ── Pet ──
{ match: ["pet", "宠物"], fields: [
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
{ name: "species", type: "VARCHAR(32)" },
{ name: "breed", type: "VARCHAR(64)" },
{ name: "birth_date", type: "DATE" },
{ name: "weight_kg", type: "DECIMAL(5,2)" },
{ name: "avatar_url", type: "TEXT" },
]},
{ match: ["album", "相册"], fields: [
{ name: "title", type: "VARCHAR(128)" },
{ name: "image_url", type: "TEXT", constraints: "NOT NULL" },
{ name: "caption", type: "TEXT" },
{ name: "taken_at", type: "TIMESTAMPTZ" },
]},
{ match: ["hospital", "医院"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "address", type: "TEXT" },
{ name: "phone", type: "VARCHAR(20)" },
{ name: "rating", type: "DECIMAL(2,1)" },
{ name: "lat", type: "DECIMAL(9,6)" },
{ name: "lng", type: "DECIMAL(9,6)" },
]},
// ── Blog / CMS ──
{ match: ["article", "文章"], fields: [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "content", type: "TEXT" },
{ name: "excerpt", type: "TEXT" },
{ name: "cover_url", type: "TEXT" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'draft'" },
{ name: "author_id", type: "UUID", constraints: "FK → users.id" },
{ name: "category", type: "VARCHAR(64)" },
{ name: "published_at", type: "TIMESTAMPTZ" },
]},
{ match: ["comment", "评论"], fields: [
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
{ name: "entity_id", type: "UUID", constraints: "NOT NULL" },
{ name: "content", type: "TEXT", constraints: "NOT NULL" },
{ name: "parent_id", type: "UUID", constraints: "FK → comments.id (自引用)" },
]},
{ match: ["media", "媒体"], fields: [
{ name: "filename", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "url", type: "TEXT", constraints: "NOT NULL" },
{ name: "mime_type", type: "VARCHAR(64)" },
{ name: "size_bytes", type: "INTEGER" },
]},
// ── Tags (通用) ──
{ match: ["tag", "标签"], fields: [
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL, UNIQUE" },
{ name: "color", type: "VARCHAR(7)" },
]},
// ── Stats / Dashboard ──
{ match: ["stat", "统计", "dashboard", "仪表盘"], fields: [
{ name: "metric", type: "VARCHAR(64)", constraints: "NOT NULL" },
{ name: "value", type: "DECIMAL(12,2)" },
{ name: "period", type: "VARCHAR(32)" },
{ name: "recorded_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
// ── Contract ──
{ match: ["contract", "合同"], fields: [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "party_a", type: "VARCHAR(128)" },
{ name: "party_b", type: "VARCHAR(128)" },
{ name: "amount", type: "DECIMAL(14,2)" },
{ name: "signed_at", type: "DATE" },
{ name: "expires_at", type: "DATE" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'draft'" },
{ name: "file_url", type: "TEXT" },
]},
// ── Approval / Reminder ──
{ match: ["approval", "审批"], exclude: ["stock_out", "出库审批"], fields: [
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
{ name: "entity_id", type: "UUID" },
{ name: "applicant_id", type: "UUID", constraints: "FK → users.id" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
{ name: "form_data", type: "JSONB" },
]},
{ match: ["reminder", "提醒", "到期提醒"], fields: [
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
{ name: "entity_id", type: "UUID" },
{ name: "remind_at", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
{ name: "message", type: "TEXT" },
{ name: "sent", type: "BOOLEAN", constraints: "DEFAULT false" },
]},
// ── Inspection ──
{ match: ["inspection_plan", "巡检计划"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "frequency", type: "VARCHAR(32)" },
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
{ name: "next_run", type: "TIMESTAMPTZ" },
{ name: "active", type: "BOOLEAN", constraints: "DEFAULT true" },
]},
{ match: ["inspection_record", "巡检记录"], fields: [
{ name: "plan_id", type: "UUID", constraints: "FK → inspection_plans.id" },
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
{ name: "inspector_id", type: "UUID", constraints: "FK → users.id" },
{ name: "result", type: "VARCHAR(32)" },
{ name: "notes", type: "TEXT" },
{ name: "inspected_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
{ match: ["fault", "故障"], fields: [
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
{ name: "reported_by", type: "UUID", constraints: "FK → users.id" },
{ name: "description", type: "TEXT", constraints: "NOT NULL" },
{ name: "severity", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'open'" },
{ name: "resolved_at", type: "TIMESTAMPTZ" },
]},
{ match: ["equipment", "设备"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "code", type: "VARCHAR(64)", constraints: "UNIQUE" },
{ name: "location", type: "VARCHAR(128)" },
{ name: "model", type: "VARCHAR(64)" },
{ name: "installed_at", type: "DATE" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'active'" },
]},
// ── Callback / Satisfaction ──
{ match: ["callback_plan", "回访计划"], fields: [
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
{ name: "scheduled_at", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
{ name: "assigned_to", type: "UUID", constraints: "FK → users.id" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
]},
{ match: ["callback_record", "回访记录"], fields: [
{ name: "plan_id", type: "UUID", constraints: "FK → callback_plans.id" },
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
{ name: "content", type: "TEXT" },
{ name: "result", type: "VARCHAR(32)" },
{ name: "visited_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
{ match: ["satisfaction", "满意度"], fields: [
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
{ name: "callback_record_id", type: "UUID", constraints: "FK → callback_records.id" },
{ name: "rating", type: "INTEGER", constraints: "CHECK (rating >= 1 AND rating <= 5)" },
{ name: "feedback", type: "TEXT" },
]},
// ── Warehouse / Stock ──
{ match: ["stock_in", "入库"], fields: [
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "quantity", type: "INTEGER", constraints: "NOT NULL" },
{ name: "supplier", type: "VARCHAR(128)" },
{ name: "received_by", type: "UUID", constraints: "FK → users.id" },
{ name: "received_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
{ match: ["stock_out", "出库"], fields: [
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "quantity", type: "INTEGER", constraints: "NOT NULL" },
{ name: "recipient", type: "VARCHAR(128)" },
{ name: "approved_by", type: "UUID", constraints: "FK → users.id" },
{ name: "shipped_at", type: "TIMESTAMPTZ" },
]},
{ match: ["stocktaking", "盘点"], fields: [
{ name: "warehouse", type: "VARCHAR(64)" },
{ name: "product_name", type: "VARCHAR(128)" },
{ name: "expected_qty", type: "INTEGER" },
{ name: "actual_qty", type: "INTEGER" },
{ name: "diff", type: "INTEGER" },
{ name: "stocktaken_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
{ match: ["stock_alert", "库存预警"], fields: [
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "current_qty", type: "INTEGER" },
{ name: "threshold", type: "INTEGER" },
{ name: "alerted_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
]},
// ── Classroom / Schedule ──
{ match: ["classroom", "教室"], fields: [
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
{ name: "building", type: "VARCHAR(64)" },
{ name: "capacity", type: "INTEGER" },
{ name: "equipment", type: "JSONB" },
]},
{ match: ["schedule", "排课", "课表"], fields: [
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
{ name: "classroom_id", type: "UUID", constraints: "FK → classrooms.id" },
{ name: "instructor_id", type: "UUID", constraints: "FK → users.id" },
{ name: "day_of_week", type: "INTEGER", constraints: "CHECK (day_of_week >= 0 AND day_of_week <= 6)" },
{ name: "start_time", type: "TIME" },
{ name: "end_time", type: "TIME" },
]},
];
/**
* Match a table name against field templates to get domain-specific fields.
* Returns the best-matching template's fields, or generic fields as fallback.
*/
function getDomainFields(tableName, featureName) {
const lower = tableName.toLowerCase();
const featureLower = (featureName || "").toLowerCase();
for (const template of FIELD_TEMPLATES) {
// Check exclusions first
if (template.exclude && template.exclude.some(ex => lower === ex || lower.includes(ex) || featureLower.includes(ex))) {
continue;
}
for (const keyword of template.match) {
if (lower === keyword || lower.includes(keyword) || featureLower.includes(keyword)) {
return template.fields;
}
}
}
// Generic fallback
return [
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "description", type: "TEXT" },
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'active'" },
{ name: "data", type: "JSONB" },
];
}
function deriveTableFromFeature(feature) {
const tableName = toSafeSlug(feature.name);
if (tableName === "users" || tableName === "auth") return null;
const domainFields = getDomainFields(tableName, feature.name);
// Deduplicate: domain fields override base fields with same name
const baseNames = new Set(["id", "user_id", "created_at", "updated_at"]);
const merged = [
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
...domainFields.filter(f => !baseNames.has(f.name)),
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
];
return {
table: tableName,
description: (feature.description || feature.name) + "表",
fields: merged,
indexes: ["idx_" + tableName + "_user ON " + tableName + "(user_id)"],
};
}
export function generateDatabaseSchema(features, apis, domain) {
const tables = [];
// Always: users table
tables.push({
table: "users",
description: "用户表",
fields: [
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
{ name: "phone", type: "VARCHAR(20)", constraints: "UNIQUE" },
{ name: "nickname", type: "VARCHAR(64)" },
{ name: "avatar_url", type: "TEXT" },
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
],
indexes: ["idx_users_phone ON users(phone)"],
});
// Resource tables derived from API paths
const resourceMap = new Map();
for (const api of apis) {
// Extract resource name from path: /api/pets → pets, /api/orders → orders
const match = api.path.match(/^\/api\/([^/:]+)/);
if (match) {
const resource = match[1];
if (resource === "auth" || resource === "health") continue;
if (!resourceMap.has(resource)) resourceMap.set(resource, []);
resourceMap.get(resource).push(api);
}
}
// Derive tables from features (P0 fix: user input > domain template)
const derivedTableNames = new Set();
for (const feature of features) {
const table = deriveTableFromFeature(feature);
if (table && !derivedTableNames.has(table.table)) {
derivedTableNames.add(table.table);
tables.push(table);
}
}
// Also add tables from API paths not covered by features
for (const [resource] of resourceMap) {
if (!derivedTableNames.has(resource) && resource !== "users") {
derivedTableNames.add(resource);
tables.push({
table: resource,
description: resource + "表",
fields: [
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
{ name: "data", type: "JSONB" },
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
],
});
}
}
return tables;
}
// ═══════════════════════════════════════════════════════
// 6. API Design Generator
// ═══════════════════════════════════════════════════════
/**
* Group and enrich API requirements.
*
* @param {object[]} apiRequirements
* @returns {object[]}
*/
export function designAPI(apiRequirements) {
// Group APIs by resource
const resourceMap = new Map();
for (const api of apiRequirements) {
const match = api.path.match(/^\/api\/([^/:]+)/);
const resource = match ? match[1] : "misc";
if (!resourceMap.has(resource)) {
resourceMap.set(resource, {
resource,
basePath: `/api/${resource}`,
endpoints: [],
});
}
resourceMap.get(resource).endpoints.push({
method: api.method,
path: api.path,
description: api.description,
});
}
// Sort resources alphabetically
return Array.from(resourceMap.values()).sort((a, b) => a.resource.localeCompare(b.resource));
}
// ═══════════════════════════════════════════════════════
// 7. Directory Structure Generator
// ═══════════════════════════════════════════════════════
/**
* Generate a project directory structure from the architecture.
*
* @param {string} projectName
* @param {string} domain
* @param {object[]} modules
* @returns {string[]} — Lines of directory tree text
*/
export function generateDirectoryStructure(projectName, domain, modules) {
const dirName = projectName.toLowerCase();
const lines = [];
lines.push(`${dirName}/`);
lines.push(`├── apps/`);
lines.push(`│ ├── mobile/ # React Native / Flutter 移动端`);
lines.push(`│ │ ├── src/`);
lines.push(`│ │ │ ├── screens/ # 页面组件`);
// Screen directories from modules
for (const mod of modules.slice(0, 4)) {
lines.push(`│ │ │ │ ├── ${mod.name}/`);
}
lines.push(`│ │ │ ├── components/ # 通用组件`);
lines.push(`│ │ │ ├── hooks/ # 自定义 Hooks`);
lines.push(`│ │ │ ├── services/ # API 调用层`);
lines.push(`│ │ │ ├── store/ # 状态管理`);
lines.push(`│ │ │ └── utils/ # 工具函数`);
lines.push(`│ │ ├── app.json`);
lines.push(`│ │ └── package.json`);
lines.push(`│ └── web/ # Web 管理后台`);
lines.push(`│ ├── src/`);
lines.push(`│ │ ├── pages/`);
lines.push(`│ │ ├── components/`);
lines.push(`│ │ └── layouts/`);
lines.push(`│ └── package.json`);
lines.push(`├── packages/`);
lines.push(`│ └── shared/ # 共享类型/常量`);
// Backend
lines.push(`├── server/ # NestJS 后端服务`);
lines.push(`│ ├── src/`);
lines.push(`│ │ ├── modules/ # 业务模块`);
for (const mod of modules.slice(0, 5)) {
const modName = mod.name.replace(/\s+/g, "-").toLowerCase();
lines.push(`│ │ │ ├── ${modName}/`);
lines.push(`│ │ │ │ ├── ${modName}.controller.ts`);
lines.push(`│ │ │ │ ├── ${modName}.service.ts`);
lines.push(`│ │ │ │ ├── ${modName}.module.ts`);
lines.push(`│ │ │ │ └── dto/`);
}
lines.push(`│ │ ├── common/ # 通用(guards, filters, interceptors`);
lines.push(`│ │ ├── prisma/ # Prisma ORM`);
lines.push(`│ │ │ └── schema.prisma`);
lines.push(`│ │ ├── config/ # 环境配置`);
lines.push(`│ │ └── main.ts`);
lines.push(`│ ├── test/`);
lines.push(`│ ├── Dockerfile`);
lines.push(`│ └── package.json`);
// Root-level files
lines.push(`├── docker-compose.yml`);
lines.push(`├── .github/workflows/ # CI/CD`);
lines.push(`│ └── ci.yml`);
lines.push(`├── .env.example`);
lines.push(`├── README.md`);
lines.push(`└── turbo.json # Monorepo 配置`);
return lines;
}
// ═══════════════════════════════════════════════════════
// 8. Main Architecture Pipeline
// ═══════════════════════════════════════════════════════
/**
* Resolve tech stack from PRD.
*
* @param {object} prd
* @returns {object}
*/
export function resolveTechStack(prd) {
const platforms = prd.techConstraints?.platforms || ["web"];
const constraints = prd.techConstraints?.considerations || [];
const extraFeatures = prd.extraFeatures || [];
const recommended = prd.techConstraints?.recommendedStack || "";
// Determine platform key
let key = "default";
if (platforms.includes("wechat-miniapp")) key = "wechat-miniapp";
else if (platforms.includes("miniapp")) key = "miniapp";
else if (platforms.length === 1 && platforms[0] === "ios") key = "ios-only";
else if (platforms.length === 1 && platforms[0] === "android") key = "android-only";
else if (platforms.length === 1 && platforms[0] === "web") key = "web-only";
else if (platforms.includes("mobile") && platforms.includes("web")) key = "mobile-web";
else if (platforms.includes("mobile")) key = "mobile";
const stack = { ...TECH_STACKS[key] };
// Extra feature adjustments
if (extraFeatures.includes("wechat-pay") || extraFeatures.includes("alipay")) {
stack.considerations = stack.considerations || [];
stack.considerations.push("需集成第三方支付 SDK");
}
if (extraFeatures.includes("logistics-tracking")) {
stack.considerations = stack.considerations || [];
stack.considerations.push("需集成物流追踪 API(如快递鸟)");
}
if (extraFeatures.includes("real-time")) {
stack.backend += " + WebSocket (Socket.io)";
}
if (extraFeatures.includes("ai-powered")) {
stack.backend += " + AI 服务(OpenAI / 文心)";
}
return stack;
}
/**
* Generate full architecture from PRD.
*
* @param {object} prd — PRD from SF-01
* @returns {object}
*/
export function generateArchitecture(prd) {
// Validate input
if (!prd || prd.error) {
return {
error: "INVALID_PRD",
message: prd?.message || "Invalid PRD input",
};
}
if (!prd.projectName || !prd.features) {
return {
error: "INCOMPLETE_PRD",
message: "PRD missing required fields: projectName, features",
};
}
const {
projectName, domain = "generic", features = [], pages = [],
apiRequirements = [], techConstraints = {}, extraFeatures = [], meta = {},
} = prd;
// 1. Resolve tech stack
const stack = resolveTechStack(prd);
// 2. Decompose modules
const modules = decomposeModules(features, domain);
// 3. Generate architecture diagram
const moduleNames = modules.map(m => m.label);
const archDiagram = generateArchDiagram(projectName, stack, moduleNames);
// 4. Generate data flows
const dataFlows = generateDataFlows(pages, apiRequirements);
// 5. Generate database schema
const databaseSchema = generateDatabaseSchema(features, apiRequirements, domain);
// 6. Design API
const apiDesign = designAPI(apiRequirements);
// 7. Generate directory structure
const directoryStructure = generateDirectoryStructure(projectName, domain, modules);
// 8. Deployment strategy
const deployment = {
environments: ["development", "staging", "production"],
strategy: stack.deployment || "Docker + Nginx",
services: [
"API Server (NestJS)",
"PostgreSQL 15",
"Redis 7",
...(extraFeatures.includes("real-time") ? ["WebSocket Server"] : []),
],
ci: "GitHub Actions → Build → Test → Deploy",
};
// Generate contract AFTER schema is built
const contractArch = { databaseSchema };
const contract = createContract(prd, contractArch);
return {
projectName,
domain,
contract,
techStack: {
frontend: stack.frontend,
backend: stack.backend,
database: stack.database,
deployment: stack.deployment,
considerations: stack.considerations || [],
},
architectureDiagram: archDiagram,
dataFlows,
modules: modules.map(m => ({
name: m.name,
label: m.label,
features: m.features,
responsibilities: m.features.map(f => `提供 ${f} 相关功能`),
})),
databaseSchema,
apiDesign,
directoryStructure,
deployment,
meta: {
generatedAt: new Date().toISOString(),
sourceDomain: domain,
moduleCount: modules.length,
tableCount: databaseSchema.length,
apiEndpointCount: apiRequirements.length,
},
};
}
// ═══════════════════════════════════════════════════════
// 9. File I/O
// ═══════════════════════════════════════════════════════
export function loadPRD(path) {
try {
if (!existsSync(path)) {
return { data: null, error: `PRD file not found: ${path}` };
}
const raw = readFileSync(path, "utf-8");
return { data: JSON.parse(raw), error: null };
} catch (e) {
return { data: null, error: `Failed to load PRD: ${e.message}` };
}
}
export function writeArchitecture(architecture, outputPath, pretty = false) {
const dir = dirname(outputPath);
mkdirSync(dir, { recursive: true });
const content = pretty ? JSON.stringify(architecture, null, 2) : JSON.stringify(architecture);
writeFileSync(outputPath, content);
}
// ═══════════════════════════════════════════════════════
// 10. CLI Entry
// ═══════════════════════════════════════════════════════
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
input: null, inputText: null, output: null,
pretty: false, verbose: false, help: false,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--input" && args[i + 1]) opts.input = args[++i];
else if (args[i] === "--input-text" && args[i + 1]) opts.inputText = args[++i];
else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
else if (args[i] === "--pretty") opts.pretty = true;
else if (args[i] === "--verbose") opts.verbose = true;
else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
}
return opts;
}
function printJSON(data, pretty) {
console.log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
}
async function main() {
const opts = parseArgs();
if (opts.help) {
console.log(`
Architecture Agent — SF-02
Usage:
node scripts/architecture-agent.mjs --input <prd.json> [options]
node scripts/architecture-agent.mjs --input-text "<需求>" [options]
Options:
--input <path> PRD JSON 文件路径(SF-01 输出)
--input-text <text> 直接传入需求(自动调 SF-01 生成 PRD)
--output <path> 输出架构 JSON 文件路径
--pretty Format JSON with indentation
--verbose 详细输出
--help 显示帮助
Examples:
node scripts/architecture-agent.mjs --input prd-example.json --pretty
node scripts/architecture-agent.mjs --input-text "做一个电商小程序" --output architecture.json
`);
return;
}
let prd = null;
if (opts.input) {
const { data, error } = loadPRD(opts.input);
if (error) { console.error(error); process.exit(1); }
prd = data;
} else if (opts.inputText) {
// Auto-generate PRD from SF-01
try {
const mod = await import("./project-intake-agent.mjs");
prd = mod.generatePRD(opts.inputText);
if (prd.error) {
console.error(`SF-01 PRD generation failed: ${prd.message}`);
process.exit(1);
}
} catch (e) {
console.error(`Failed to load SF-01 agent: ${e.message}`);
process.exit(1);
}
}
if (!prd) {
console.error("Error: --input <path> or --input-text <text> is required. Use --help for usage.");
process.exit(1);
}
const architecture = generateArchitecture(prd);
if (architecture.error) {
console.error(`Architecture generation failed: ${architecture.message}`);
process.exit(1);
}
if (opts.verbose) {
console.error(`Project: ${architecture.projectName}`);
console.error(`Domain: ${architecture.domain}`);
console.error(`Modules: ${architecture.meta.moduleCount}`);
console.error(`Tables: ${architecture.meta.tableCount}`);
console.error(`API Endpoints: ${architecture.meta.apiEndpointCount}`);
}
if (opts.output) {
writeArchitecture(architecture, resolve(opts.output), opts.pretty);
if (opts.verbose) console.error(`Architecture written to: ${opts.output}`);
}
printJSON({
projectName: architecture.projectName,
domain: architecture.domain,
techStack: architecture.techStack,
moduleCount: architecture.meta.moduleCount,
tableCount: architecture.meta.tableCount,
apiEndpointCount: architecture.meta.apiEndpointCount,
outputPath: opts.output || null,
}, opts.pretty);
}
if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) {
main();
}