init git
This commit is contained in:
336
.claude/workflows/generate-sad.js
Normal file
336
.claude/workflows/generate-sad.js
Normal 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
|
||||
Reference in New Issue
Block a user