export const meta = { name: 'generate-sad', description: 'Sinh tài liệu SAD theo từng stage có cổng phê duyệt: intake làm rõ brief → requirements → architecture → API/Data/UI-UX → detailed → security → test&ops → consolidate. Mỗi lần gọi chạy 1 stage (args.stage) rồi trả summary để người dùng duyệt.', whenToUse: 'Gọi từ skill sad-pipeline. Dùng args.stage để chạy từng bước; stage "all" chạy liền không có gate.', phases: [ { title: 'Intake', detail: 'Đánh giá độ đủ thông tin của brief, sinh câu hỏi làm rõ' }, { title: 'Requirements', detail: 'Mục 1-2: Tổng quan & phân tích yêu cầu' }, { title: 'Architecture', detail: 'Mục 3: Kiến trúc hệ thống' }, { title: 'Design Fanout', detail: 'Mục 4, 5, 7: API / Data / UI-UX song song' }, { title: 'Detailed Design', detail: 'Mục 6: Luồng xử lý chi tiết' }, { title: 'Security', detail: 'Mục 8: Rà soát bảo mật' }, { title: 'Test & Ops', detail: 'Mục 9: Kế hoạch kiểm thử & vận hành' }, { title: 'Consolidate', detail: 'Mục 0 + ráp tài liệu + kiểm tra nhất quán' }, ], } // --------------------------------------------------------------------------- // Tham số // stage: intake | requirements | architecture | fanout | detailed | // security | testops | consolidate | all (mặc định: all) // brief: mô tả dự án (stage intake, lần đầu) // answers: câu trả lời của người dùng cho câu hỏi làm rõ (stage intake, vòng sau) // round: số vòng làm rõ hiện tại (stage intake; >=3 sẽ tự chốt mặc định) // notes: ghi chú của người duyệt khi chạy lại một stage (Revise) // only: ['api','data','uiux'] — chỉ chạy một phần fanout // requirementIds: danh sách FR-xx đã duyệt, để tính coverage bằng code // sectionsDir / briefFile / outFile: đường dẫn (mặc định docs/...) // --------------------------------------------------------------------------- const a = args && typeof args === 'object' ? args : { brief: args } const STAGES = ['intake', 'requirements', 'architecture', 'fanout', 'detailed', 'security', 'testops', 'consolidate'] const stage = a.stage || 'all' if (stage !== 'all' && !STAGES.includes(stage)) { throw new Error(`args.stage không hợp lệ: "${stage}". Hợp lệ: all | ${STAGES.join(' | ')}`) } const S = a.sectionsDir || 'docs/sections' const BRIEF = a.briefFile || 'docs/00-project-brief.md' const OUT = a.outFile || 'docs/SAD.md' const only = Array.isArray(a.only) ? a.only : null const round = Number(a.round) || 1 const notes = stage !== 'all' && a.notes ? `\n\n## Ghi chú từ người duyệt (bắt buộc xử lý trước khi làm gì khác)\n${a.notes}\n` : '' let requirementIds = Array.isArray(a.requirementIds) ? a.requirementIds.slice() : [] const run = (s) => stage === 'all' || stage === s // --------------------------------------------------------------------------- // Schema — mọi agent trả structured output để script rẽ nhánh bằng code // --------------------------------------------------------------------------- const GAP = { type: 'object', properties: { field: { type: 'string' }, severity: { type: 'string', enum: ['Critical', 'Important', 'Nice-to-have'] }, question: { type: 'string' }, proposedDefault: { type: 'string' }, riskIfAssumed: { type: 'string' }, relatedSections: { type: 'array', items: { type: 'string' } }, }, required: ['field', 'severity', 'question', 'proposedDefault'], } const INTAKE_SCHEMA = { type: 'object', properties: { ready: { type: 'boolean' }, completenessScore: { type: 'number' }, briefVersion: { type: 'number' }, profile: { type: 'object', properties: { scale: { type: 'string' }, hasPayment: { type: 'boolean' }, hasPII: { type: 'boolean' }, platforms: { type: 'array', items: { type: 'string' } }, integrations: { type: 'array', items: { type: 'string' } }, notApplicableSections: { type: 'array', items: { type: 'string' } }, }, }, gaps: { type: 'array', items: GAP }, adoptedDefaults: { type: 'array', items: { type: 'string' } }, referenceModelSummary: { type: 'string' }, summary: { type: 'string' }, }, required: ['ready', 'gaps', 'profile', 'summary'], } const FINDING = { type: 'object', properties: { targetSection: { type: 'string' }, issue: { type: 'string' }, suggestion: { type: 'string' }, severity: { type: 'string', enum: ['high', 'medium', 'low'] }, }, required: ['targetSection', 'issue', 'severity'], } const SECTION_PROPS = { filesWritten: { type: 'array', items: { type: 'string' } }, coveredRequirements: { type: 'array', items: { type: 'string' } }, knownRequirementIds: { type: 'array', items: { type: 'string' } }, assumptions: { type: 'array', items: { type: 'string' } }, openQuestions: { type: 'array', items: { type: 'string' } }, findings: { type: 'array', items: FINDING }, confidence: { type: 'string', enum: ['high', 'medium', 'low'] }, summary: { type: 'string' }, } const SECTION_REQUIRED = ['filesWritten', 'coveredRequirements', 'assumptions', 'openQuestions', 'confidence', 'summary'] const SECTION_SCHEMA = { type: 'object', properties: SECTION_PROPS, required: SECTION_REQUIRED } const REQ_SCHEMA = { type: 'object', properties: { ...SECTION_PROPS, requirements: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, priority: { type: 'string' } }, required: ['id', 'title'], }, }, entities: { type: 'array', items: { type: 'string' } }, }, required: [...SECTION_REQUIRED, 'requirements'], } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function coverage(res) { if (!res) return null const known = requirementIds.length ? requirementIds : (res.knownRequirementIds || []) const covered = new Set(res.coveredRequirements || []) return { known: known.length, covered: known.filter((id) => covered.has(id)).length, uncovered: known.filter((id) => !covered.has(id)), } } function gate(name, res, extra) { const r = res || {} const out = { stage: name, ok: !!res, filesWritten: r.filesWritten || [], coverage: coverage(res), confidence: r.confidence || 'unknown', assumptions: r.assumptions || [], openQuestions: r.openQuestions || [], findings: r.findings || [], summary: r.summary || '', requirementIds, } return extra ? Object.assign(out, extra) : out } const common = `Tuân thủ đầy đủ "Quy ước chung của pipeline" trong system prompt của bạn: đọc ${BRIEF} trước tiên, ` + `ghi frontmatter (section/title/status/version/reviewer_notes) đầu file output, right-size theo profile, ` + `và xử lý ghi chú người duyệt nếu có.` const reqHint = () => requirementIds.length ? `\nDanh sách FR đã duyệt cần phủ: ${requirementIds.join(', ')}. Trong coveredRequirements chỉ ghi mã bạn thực sự đã đề cập.` : `\nĐọc mã FR-xx từ ${S}/02-phan-tich-yeu-cau.md; trả knownRequirementIds = toàn bộ mã tìm thấy và coveredRequirements = mã bạn đã đề cập.` const report = { stage, gates: [] } if (stage === 'all') log('Chế độ "all": chạy liền toàn bộ pipeline, KHÔNG có cổng phê duyệt giữa các bước.') // --------------------------------------------------------------------------- // Stage 0 — Intake // --------------------------------------------------------------------------- if (run('intake')) { phase('Intake') const parts = [ `Bạn đang ở vòng làm rõ số ${round}. File brief: ${BRIEF} (tạo mới nếu chưa có; nếu đã có thì đọc, gộp thông tin mới và tăng version).`, a.brief ? `Mô tả dự án do người dùng cung cấp:\n"""\n${a.brief}\n"""` : `Không có brief mới trong prompt — đọc ${BRIEF} hiện có để đánh giá lại.`, a.answers ? `Câu trả lời của người dùng cho các câu hỏi vòng trước (ghép vào Q&A log; câu nào người dùng chọn "dùng mặc định" thì đưa vào "Giả định đã chốt"):\n"""\n${a.answers}\n"""` : '', round >= 3 ? 'ĐÂY LÀ VÒNG CUỐI: với mọi khoảng trống còn lại, áp dụng proposedDefault, ghi vào "Giả định đã chốt" kèm rủi ro, và trả ready=true (trừ khi hoàn toàn không rõ hệ thống làm gì).' : '', ].filter(Boolean).join('\n\n') const intake = await agent(parts, { agentType: 'intake-analyst', label: 'intake-analyst', schema: INTAKE_SCHEMA }) report.intake = intake if (!intake || !intake.ready) { log(`Brief chưa đủ thông tin (${intake ? intake.gaps.length : '?'} khoảng trống) — dừng để hỏi người dùng.`) return { stage: 'intake', ready: false, round, briefFile: BRIEF, intake } } log('Brief đã đủ thông tin để phân tích.') if (stage === 'intake') return { stage: 'intake', ready: true, round, briefFile: BRIEF, intake } } // --------------------------------------------------------------------------- // Stage 1 — Requirements (mục 1, 2) // --------------------------------------------------------------------------- if (run('requirements')) { phase('Requirements') const req = await agent( `${common}\nSoạn mục 1 và mục 2 dựa trên ${BRIEF} (bao gồm mô hình tham chiếu đã xác nhận và giả định đã chốt). ` + `Ghi ra ${S}/01-tong-quan.md và ${S}/02-phan-tich-yeu-cau.md. ` + `Trong kết quả trả về, liệt kê đầy đủ requirements (id dạng FR-xx, title, priority) và entities (danh từ nghiệp vụ chuẩn hoá từ Glossary) để các mục sau dùng nhất quán.${notes}`, { agentType: 'requirements-analyst', label: 'requirements-analyst', schema: REQ_SCHEMA }, ) if (req && Array.isArray(req.requirements)) requirementIds = req.requirements.map((r) => r.id) const g = gate('requirements', req, { requirements: req ? req.requirements || [] : [], entities: req ? req.entities || [] : [], }) report.gates.push(g) if (stage === 'requirements') return g } // --------------------------------------------------------------------------- // Stage 2 — Architecture (mục 3) // --------------------------------------------------------------------------- if (run('architecture')) { phase('Architecture') const arch = await agent( `${common}\nĐọc ${S}/01-tong-quan.md và ${S}/02-phan-tich-yeu-cau.md, sau đó soạn mục 3. ` + `Ghi ra ${S}/03-kien-truc.md.${reqHint()}${notes}`, { agentType: 'architecture-designer', label: 'architecture-designer', schema: SECTION_SCHEMA }, ) const g = gate('architecture', arch) report.gates.push(g) if (stage === 'architecture') return g } // --------------------------------------------------------------------------- // Stage 3 — Design fanout (mục 4, 5, 7 song song) // --------------------------------------------------------------------------- if (run('fanout')) { phase('Design Fanout') const want = (k) => !only || only.includes(k) const tasks = [] if (want('api')) { tasks.push(() => agent( `${common}\nĐọc ${S}/01-tong-quan.md (Glossary), ${S}/02-phan-tich-yeu-cau.md và ${S}/03-kien-truc.md, sau đó soạn mục 4. ` + `Ghi ra ${S}/04-api-design.md.${reqHint()}${notes}`, { agentType: 'api-designer', label: 'api-designer', phase: 'Design Fanout', schema: SECTION_SCHEMA }, ).then((r) => ['api', r]), ) } if (want('data')) { tasks.push(() => agent( `${common}\nĐọc ${S}/01-tong-quan.md (Glossary), ${S}/02-phan-tich-yeu-cau.md và ${S}/03-kien-truc.md, sau đó soạn mục 5. ` + `Ghi ra ${S}/05-thiet-ke-du-lieu.md.${reqHint()}${notes}`, { agentType: 'data-modeler', label: 'data-modeler', phase: 'Design Fanout', schema: SECTION_SCHEMA }, ).then((r) => ['data', r]), ) } if (want('uiux')) { tasks.push(() => agent( `${common}\nĐọc ${S}/01-tong-quan.md và ${S}/02-phan-tich-yeu-cau.md, sau đó soạn mục 7. ` + `Ghi ra ${S}/07-giao-dien.md.${reqHint()}${notes}`, { agentType: 'uiux-designer', label: 'uiux-designer', phase: 'Design Fanout', schema: SECTION_SCHEMA }, ).then((r) => ['uiux', r]), ) } if (!tasks.length) throw new Error('args.only không khớp: dùng các giá trị api | data | uiux') const done = (await parallel(tasks)).filter(Boolean) const results = {} for (const [k, r] of done) results[k] = gate(k, r) const g = { stage: 'fanout', ok: done.length === tasks.length && done.every(([, r]) => !!r), results, requirementIds } report.gates.push(g) if (stage === 'fanout') return g } // --------------------------------------------------------------------------- // Stage 4 — Detailed design (mục 6) // --------------------------------------------------------------------------- if (run('detailed')) { phase('Detailed Design') const det = await agent( `${common}\nĐọc ${S}/02-phan-tich-yeu-cau.md, ${S}/04-api-design.md và ${S}/05-thiet-ke-du-lieu.md, sau đó soạn mục 6. ` + `Ghi ra ${S}/06-luong-xu-ly.md.${reqHint()}${notes}`, { agentType: 'detailed-designer', label: 'detailed-designer', schema: SECTION_SCHEMA }, ) const g = gate('detailed', det) report.gates.push(g) if (stage === 'detailed') return g } // --------------------------------------------------------------------------- // Stage 5 — Security (mục 8, rà soát chéo) // --------------------------------------------------------------------------- if (run('security')) { phase('Security') const sec = await agent( `${common}\nĐọc ${S}/02-phan-tich-yeu-cau.md, ${S}/03-kien-truc.md, ${S}/04-api-design.md, ${S}/05-thiet-ke-du-lieu.md ` + `và ${S}/06-luong-xu-ly.md, sau đó soạn mục 8. Ghi ra ${S}/08-bao-mat.md. ` + `Mọi thiếu sót bảo mật phát hiện ở mục khác phải trả về trong findings (targetSection = số mục, VD "04").${reqHint()}${notes}`, { agentType: 'security-architect', label: 'security-architect', schema: SECTION_SCHEMA }, ) const g = gate('security', sec) report.gates.push(g) if (stage === 'security') return g } // --------------------------------------------------------------------------- // Stage 6 — Test & Ops (mục 9) // --------------------------------------------------------------------------- if (run('testops')) { phase('Test & Ops') const ops = await agent( `${common}\nĐọc toàn bộ file 01–08 trong ${S}/, sau đó soạn mục 9. Ghi ra ${S}/09-van-hanh-kiem-thu.md. ` + `Mỗi Test Case phải gắn đúng 1 mã FR-xx; coveredRequirements = các FR đã có test case.${reqHint()}${notes}`, { agentType: 'test-ops-planner', label: 'test-ops-planner', schema: SECTION_SCHEMA }, ) const g = gate('testops', ops) report.gates.push(g) if (stage === 'testops') return g } // --------------------------------------------------------------------------- // Stage 7 — Consolidate (mục 0 + ráp + rà soát) // --------------------------------------------------------------------------- if (run('consolidate')) { phase('Consolidate') const fin = await agent( `${common}\nĐọc ${BRIEF} và toàn bộ file 01–09 trong ${S}/ (file có thể có hoặc không có frontmatter — nếu không có, coi status là "unknown"). ` + `Soạn mục 0 (Document Control, kèm bảng trạng thái duyệt của từng mục), rà soát nhất quán/traceability xuyên suốt, ` + `và ráp toàn bộ thành 1 file hoàn chỉnh tại ${OUT} đúng cấu trúc introduction.md. ` + `Mọi mâu thuẫn/thiếu sót phát hiện trả về trong findings (targetSection = số mục); summary = "Ghi chú rà soát" ngắn gọn.${reqHint()}${notes}`, { agentType: 'doc-consolidator', label: 'doc-consolidator', schema: SECTION_SCHEMA }, ) const g = gate('consolidate', fin, { outFile: OUT }) report.gates.push(g) if (stage === 'consolidate') return g } return report