Files
sys-analysis-design/.claude/workflows/generate-sad.js
Leonard-ThindPad-P50 c81f249920 init git
2026-09-08 10:26:21 +07:00

337 lines
16 KiB
JavaScript
Raw Permalink 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.

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