This commit is contained in:
Leonard-ThindPad-P50
2026-09-08 10:26:21 +07:00
commit c81f249920
169 changed files with 38726 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
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