Files
sys-analysis-design/.claude/workflows/ba-pipeline.js
2026-09-11 23:01:18 +07:00

240 lines
17 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.

export const meta = {
name: 'ba-pipeline',
description: 'Điều phối quy trình BA (ba-1…ba-5) từng bước có người duyệt: mỗi lần gọi chạy đúng MỘT stage/activity, hoặc audit (chấm gate, chỉ đọc), sign (ghi chữ ký duyệt của con người vào header), sync (INDEX). Không có chế độ chạy liền.',
whenToUse: 'Gọi từ skill ba-pipeline. Bắt buộc args.project, args.stage, args.date.',
phases: [
{ title: 'Init', detail: 'ba-lifecycle: PROFILE + INDEX' },
{ title: 'Audit', detail: 'ba-gate-auditor: chấm G1–G5 + coverage RTM (chỉ đọc)' },
{ title: 'Discovery', detail: 'ba-1: stakeholder, elicitation, RQ, BRIEF, risk' },
{ title: 'Analysis', detail: 'ba-2: process, backlog US, BR, RBAC, impact' },
{ title: 'Specification', detail: 'ba-3: SRS, PART 2, AC, mã lỗi, NFR, API, UICONV, WF (≤3 US)' },
{ title: 'Design', detail: 'ba-design (screen, sau wf): DS, HIFI, proto, FIGMA, DSPEC (≤3 US)' },
{ title: 'Delivery', detail: 'ba-4: clarification, CR, review test case, UAT' },
{ title: 'Post-release', detail: 'ba-5: release note, manual, feedback, benefit' },
{ title: 'Sign', detail: 'Ghi quyết định duyệt/baseline/revise của con người' },
{ title: 'Sync', detail: 'Cập nhật INDEX từ header thật' },
],
}
const TRACK = {
label: 'BA',
outputRoot: 'ba-output',
runner: 'ba-stage-runner',
auditor: 'ba-gate-auditor',
lifecycleSkill: 'ba-lifecycle',
traceSkill: 'ba-traceability',
indexDir: '00-index',
stages: {
discovery: { phase: 'Discovery', skill: 'ba-1-discovery', dir: '01-discovery', gate: 'G1', prevGate: null,
activities: ['stakeholder', 'elicitation', 'requirements', 'goals-scope', 'risks'] },
analysis: { phase: 'Analysis', skill: 'ba-2-analysis', dir: '02-analysis', gate: 'G2', prevGate: 'G1',
activities: ['process', 'backlog', 'rules', 'rbac', 'impact'] },
specification: { phase: 'Specification', skill: 'ba-3-specification', dir: '03-specification', gate: 'G3', prevGate: 'G2',
activities: ['uiconv', 'srs', 'part2', 'ac', 'errors', 'nfr', 'api', 'wf'], scopeHint: 'danh sách US (tối đa 3); uiconv/wf chỉ khi PRODUCT=screen, kèm args.proto nếu có prototype' },
design: { phase: 'Design', skill: 'ba-design', dir: '03-specification/design', gate: 'G3', prevGate: 'G2',
activities: ['ds', 'hifi', 'proto', 'figma', 'dspec'], orderedPrefix: ['ds', 'hifi'],
externalPrereq: 'PRODUCT = screen và đã có SRS + WF (+ UICONV) của US từ stage specification; activity ds ghi vào 00-index/ (DS_*.md + ds/), các activity khác ghi vào 03-specification/design/',
scopeHint: 'danh sách US (tối đa 3); kèm args.brand nếu có brand guideline/design system tham chiếu' },
delivery: { phase: 'Delivery', skill: 'ba-4-delivery-support', dir: '04-delivery', gate: 'G4', prevGate: 'G3',
activities: ['clarification', 'cr', 'testcase-review', 'uat'], scopeHint: 'US / câu hỏi / CR-nnn cụ thể' },
'post-release': { phase: 'Post-release', skill: 'ba-5-post-release', dir: '05-post-release', gate: 'G5', prevGate: 'G4',
activities: ['release-note', 'manual', 'feedback', 'benefit'] },
},
gates: {
G1: 'PO', G2: 'PO + Tech Lead', G3: 'PO + Tech Lead + QA (+ Designer khi PRODUCT=screen: UICONV, WF, DS, HIFI, DSPEC — hoặc PO ký thay kèm DEC khi không có Designer)', G4: 'PO', G5: 'PO + BA Lead',
},
profileFields: ['product', 'lifecycle', 'rigor'],
}
// ===========================================================================
// ENGINE (dùng chung cho ba-pipeline / sa-pipeline) — một stage mỗi lần gọi
// project (bắt buộc) · stage (bắt buộc) · date YYYY-MM-DD (bắt buộc)
// activity : một hoạt động trong stage (mặc định: cả stage, theo thứ tự)
// scope : phạm vi (VD US-012,US-013 · CR-004 · --focus)
// inputs[] : file/nguồn người dùng cung cấp (ưu tiên cao nhất)
// proto : (specification, PRODUCT=screen) prototype/design system tham chiếu cho uiconv/wf — đường dẫn, URL, hoặc mục SAD
// brand : (design, PRODUCT=screen) brand guideline / design system tham chiếu cho ba-design (token, font, component) — đường dẫn hoặc URL
// answers : trả lời của người dùng cho OQ / humanInputNeeded
// notes : ghi chú người duyệt (Revise)
// override : true = người dùng khẳng định chạy dù gate trước chưa qua
// profile : {product, lifecycle, rigor} (init, hoặc khi đã xác nhận)
// approvals[] : (sign) {artifact, decision: approve|baseline|revise, approver, role, note}
// gate, decisions[] : (sign) gate được ký, DEC cần ghi
// outputRoot : mặc định TRACK.outputRoot
// ===========================================================================
const a = args && typeof args === 'object' ? args : {}
const STAGE_NAMES = ['init', 'audit', ...Object.keys(TRACK.stages), 'sign', 'sync']
if (!a.project) throw new Error('Thiếu args.project — tên project (một project mỗi lần chạy)')
if (!STAGE_NAMES.includes(a.stage)) throw new Error(`args.stage không hợp lệ: "${a.stage}". Hợp lệ: ${STAGE_NAMES.join(' | ')}`)
if (!a.date || !/^\d{4}-\d{2}-\d{2}$/.test(a.date)) throw new Error('Thiếu args.date (YYYY-MM-DD) — header artifact cần ngày thật')
const project = a.project
const stage = a.stage
const root = `${a.outputRoot || TRACK.outputRoot}/${project}`
const skillsDir = '.claude/skills'
const def = TRACK.stages[stage] || null
if (def && a.activity && !def.activities.includes(a.activity)) {
throw new Error(`args.activity "${a.activity}" không thuộc stage ${stage}. Hợp lệ: ${def.activities.join(' | ')}`)
}
if (stage === 'init' && !a.profile && TRACK.profileFields.length) {
throw new Error(`Stage init cần args.profile {${TRACK.profileFields.join(', ')}} đã được người dùng xác nhận`)
}
if (stage === 'sign' && !(Array.isArray(a.approvals) && a.approvals.length)) {
throw new Error('Stage sign cần args.approvals[] {artifact, decision, approver, role, note?}')
}
// ---------------- Schema ----------------
const STR_ARR = { type: 'array', items: { type: 'string' } }
const obj = (props, required) => ({ type: 'object', properties: props, required })
const arr = (item) => ({ type: 'array', items: item })
const S = { type: 'string' }
const B = { type: 'boolean' }
const N = { type: 'number' }
const STAGE_SCHEMA = obj({
filesWritten: STR_ARR,
artifacts: arr(obj({ code: S, path: S, version: S, status: S, confidence: S }, ['code', 'path', 'status'])),
blocked: B,
gateWarning: S,
gateSelfCheck: arr(obj({ item: S, ok: B, note: S }, ['item', 'ok'])),
openQuestions: arr(obj({ id: S, question: S, askWho: S, blocks: S, ifReversed: S }, ['id', 'question'])),
humanInputNeeded: arr(obj({ topic: S, question: S, suggestedDefault: S }, ['topic', 'question'])),
assumptions: STR_ARR,
decisions: arr(obj({ id: S, text: S }, ['text'])),
adrs: arr(obj({ id: S, title: S, status: S, radar: N }, ['id', 'title'])),
baSyncIssues: arr(obj({ baArtifact: S, saArtifact: S, issue: S, action: S }, ['issue'])),
tbdCount: N,
ambiguousCount: N,
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
summary: S,
}, ['filesWritten', 'blocked', 'gateSelfCheck', 'openQuestions', 'humanInputNeeded', 'confidence', 'summary'])
const AUDIT_SCHEMA = obj({
profile: obj({ product: S, lifecycle: S, rigor: S, confirmed: B }, []),
gates: arr(obj({ gate: S, status: { type: 'string', enum: ['✅', '🟠', '☐'] }, artifacts: STR_ARR, missing: STR_ARR, signersRequired: S, lowConfidence: STR_ARR }, ['gate', 'status', 'missing'])),
currentPosition: S,
blockersHard: STR_ARR,
blockersSoft: STR_ARR,
coverage: arr(obj({ check: S, value: S, pass: B, details: STR_ARR }, ['check', 'pass'])),
baSync: arr(obj({ pair: S, issue: S, action: S }, ['pair', 'issue'])),
overdueOQ: arr(obj({ id: S, askWho: S, sinceDate: S, blocks: S, ifReversed: S }, ['id'])),
warnings: STR_ARR,
nextActions: arr(obj({ action: S, skill: S }, ['action'])),
summary: S,
}, ['gates', 'currentPosition', 'nextActions', 'summary'])
const SIGN_SCHEMA = obj({
filesEdited: STR_ARR,
applied: arr(obj({ artifact: S, decision: S, newStatus: S, newVersion: S }, ['artifact', 'decision'])),
refused: arr(obj({ artifact: S, reason: S }, ['artifact', 'reason'])),
indexUpdated: B,
summary: S,
}, ['filesEdited', 'applied', 'refused', 'summary'])
// ---------------- Khối prompt dùng chung ----------------
const lines = []
lines.push(`Project: ${project}. Thư mục output: ${root}/. Ngày hôm nay (dùng cho header Date/Approved by): ${a.date}.`)
lines.push(`Skill điều phối: ${skillsDir}/${TRACK.lifecycleSkill}/ (references bắt buộc) · skill truy vết: ${skillsDir}/${TRACK.traceSkill}/.`)
if (a.profile) lines.push(`Profile đã được người dùng xác nhận: ${Object.keys(a.profile).map((k) => `${k}=${a.profile[k]}`).join(' · ')}.`)
else if (TRACK.profileFields.length) lines.push(`Profile: đọc ${root}/${TRACK.indexDir}/PROFILE_${project}.md; chưa có ⇒ dùng mặc định standard và NÓI RÕ điều đó.`)
if (Array.isArray(a.inputs) && a.inputs.length) lines.push(`Input bổ sung do người dùng cung cấp (ưu tiên cao nhất):\n${a.inputs.map((i) => `- ${i}`).join('\n')}`)
if (a.proto) lines.push(`## Prototype / design system tham chiếu (cho UICONV và WF, PRODUCT=screen)\n${a.proto}\nĐây là nguồn sự thật về BỐ CỤC: WF chạy chế độ 🎨 (ghi lại + đối chiếu với SRS, ghi mọi lệch vào WF §5). Thành phần có trong prototype mà SRS không có ⇒ KHÔNG tự thêm vào SRS, ghi lệch loại Tồn tại cho PO quyết.`)
else lines.push(`Prototype tham chiếu: không được cung cấp. Với PRODUCT=screen, tự tìm theo thứ tự design/, prototype/, docs/sections/07-*.md, docs/SAD.md §7; vẫn không có ⇒ WF chạy chế độ ✏️ (đề xuất của BA, cần Designer ký) và nói rõ trong summary.`)
if (stage === 'design') {
if (a.brand) lines.push(`## Brand guideline / design system tham chiếu (cho ba-design)\n${a.brand}\nĐây là nguồn sự thật về VISUAL (token màu/chữ/khoảng cách, component). Trích token từ đây, ghi độ tin cậy 🟢 ở DS §0.`)
else lines.push(`Brand guideline: không được cung cấp. Tự tìm theo thứ tự design/, brand/, prototype/, UICONV §0, SAD §7, DEC của Tech Lead về design system; vẫn không có ⇒ dùng token trung tính của template, ghi độ tin cậy 🔴 ở DS §0 + OQ, và nói rõ trong summary. Không tự chọn màu brand.`)
lines.push(`Công cụ: runner không có Bash — tokens.css viết tay theo quy tắc --ds-<đường dẫn nối '-'>; phép kiểm design-check.mjs và chụp PNG (render-figma.sh) do người điều phối chạy sau, ghi vào humanInputNeeded nếu cần.`)
}
if (a.answers) lines.push(`## Câu trả lời của người dùng cho OQ / humanInputNeeded vòng trước\n${a.answers}`)
if (a.notes) lines.push(`## Ghi chú từ người duyệt (bắt buộc xử lý trước, version +0.1 và Change Log)\n${a.notes}`)
if (a.override) lines.push(`## Ngoại lệ gate\nNgười dùng đã được cảnh báo gate trước chưa ✅ Baselined và KHẲNG ĐỊNH LẠI muốn tiếp tục. Làm tiếp, ghi ngoại lệ vào Open Questions + DEC-nn của artifact.`)
const common = lines.join('\n\n')
const gateOf = (name) => (TRACK.stages[name] ? TRACK.stages[name].gate : null)
const signersOf = (g) => (g && TRACK.gates[g]) || null
// ---------------- Kết quả trả về cho người duyệt ----------------
function wrap(kind, res, extra) {
const r = res || {}
const out = {
track: TRACK.label, project, stage, kind, ok: !!res,
date: a.date, activity: a.activity || null, scope: a.scope || null,
gate: def ? def.gate : (a.gate || null),
signersRequired: signersOf(def ? def.gate : a.gate),
result: r,
}
return extra ? Object.assign(out, extra) : out
}
// ---------------- Stage: init ----------------
if (stage === 'init') {
phase('Init')
const r = await agent(
`${common}\n\nNhiệm vụ "init": làm đúng ${skillsDir}/${TRACK.lifecycleSkill}/SKILL.md (khởi tạo project mới). ` +
`Tạo cấu trúc thư mục trong ${root}/, file PROFILE (nếu bộ này có profile) và INDEX theo mẫu artifact-map. ` +
`Không tạo file rỗng cho giai đoạn sau. Nếu ${root}/ đã có nội dung ⇒ không ghi đè, trả blocked=true và nêu rõ.`,
{ agentType: TRACK.runner, label: `${TRACK.label}:init`, schema: STAGE_SCHEMA },
)
return wrap('init', r)
}
// ---------------- Stage: audit (chỉ đọc) ----------------
if (stage === 'audit') {
phase('Audit')
const r = await agent(
`${common}\n\nNhiệm vụ "audit" (CHỈ ĐỌC): chấm toàn bộ gate theo checklist của ${skillsDir}/${TRACK.lifecycleSkill}/references/workflow.md ` +
`(điều chỉnh theo RIGOR nếu có), chạy các phép kiểm coverage/nhất quán của ${skillsDir}/${TRACK.traceSkill}/, ` +
`liệt kê blocker cứng/mềm, OQ quá hạn (so với ngày ${a.date}), cảnh báo bắt buộc và tối đa 3 việc tiếp theo. ` +
`✅ chỉ khi có chữ ký thật trong header. Không ghi file.${a.scope ? `\nPhạm vi cần soi kỹ: ${a.scope}` : ''}`,
{ agentType: TRACK.auditor, label: `${TRACK.label}:audit`, schema: AUDIT_SCHEMA },
)
return wrap('audit', r)
}
// ---------------- Stage: sign (ghi quyết định của con người) ----------------
if (stage === 'sign') {
phase('Sign')
const list = a.approvals.map((p, i) =>
`${i + 1}. ${p.artifact} — quyết định: ${p.decision}; người duyệt: ${p.approver}${p.role ? ` (${p.role})` : ''}${p.note ? `; ghi chú: ${p.note}` : ''}`).join('\n')
const decs = Array.isArray(a.decisions) && a.decisions.length ? `\nDEC cần ghi vào sổ quyết định:\n${a.decisions.map((d) => `- ${d}`).join('\n')}` : ''
const gateLine = a.gate ? `\nGate được ký: ${a.gate} — người ký yêu cầu theo quy trình: ${signersOf(a.gate) || 'xem workflow.md'}. Nếu danh sách người duyệt bên dưới không đủ vai trò ⇒ vẫn ghi Approved by nhưng KHÔNG baseline, và nêu trong refused.` : ''
const r = await agent(
`${common}\n\nNhiệm vụ "sign": ghi quyết định duyệt của CON NGƯỜI vào header/Change Log/INDEX theo đúng quy tắc trong system prompt của bạn. ` +
`Không đổi nội dung chuyên môn. Từ chối baseline khi artifact chưa đủ điều kiện (còn TBD, thiếu header, checklist gate chưa đủ) và nêu lý do.${gateLine}\n\nDanh sách quyết định:\n${list}${decs}`,
{ agentType: TRACK.runner, label: `${TRACK.label}:sign`, schema: SIGN_SCHEMA },
)
return wrap('sign', r, { gate: a.gate || null, signersRequired: signersOf(a.gate) })
}
// ---------------- Stage: sync (INDEX) ----------------
if (stage === 'sync') {
phase('Sync')
const r = await agent(
`${common}\n\nNhiệm vụ "sync": cập nhật ${root}/${TRACK.indexDir}/INDEX_${project}.md (và sổ ADL/DTM/OQ/DECISION nếu bộ này có) ` +
`từ header THẬT của các artifact theo ${skillsDir}/${TRACK.lifecycleSkill}/references/artifact-map.md. Không sửa artifact.`,
{ agentType: TRACK.runner, label: `${TRACK.label}:sync`, schema: STAGE_SCHEMA },
)
return wrap('sync', r)
}
// ---------------- Stage giai đoạn (ba-1…/sa-1…) ----------------
phase(def.phase)
const acts = a.activity ? [a.activity] : def.activities
const prereq = [
def.prevGate ? `Gate trước cần ✅ Baselined: ${def.prevGate}.` : 'Đây là giai đoạn đầu, không có gate trước trong bộ này.',
def.externalPrereq ? `Điều kiện ngoài bộ: ${def.externalPrereq}.` : '',
def.orderedPrefix ? `Thứ tự bắt buộc trong giai đoạn: ${def.orderedPrefix.join(' → ')} phải có trước các hoạt động khác.` : '',
].filter(Boolean).join(' ')
if (def.scopeHint && !a.scope) log(`Cảnh báo: stage ${stage} thường cần args.scope (${def.scopeHint}) — agent sẽ tự chọn và báo trong summary.`)
const r = await agent(
`${common}\n\nNhiệm vụ: thực thi skill ${skillsDir}/${def.skill}/SKILL.md ở chế độ go, ` +
`chỉ các hoạt động: ${acts.join(', ')}${a.scope ? ` · phạm vi: ${a.scope}` : ''}. Output vào ${root}/${def.dir}/. ` +
`${prereq} Preflight gate trước khi ghi: chưa qua và không có "Ngoại lệ gate" ⇒ blocked=true, không ghi file. ` +
`Gate của giai đoạn này: ${def.gate} (người ký: ${signersOf(def.gate)}) — bạn chỉ TỰ CHẤM checklist, không được ghi ✅ vào header.`,
{ agentType: TRACK.runner, label: `${TRACK.label}:${stage}${a.activity ? ':' + a.activity : ''}`, schema: STAGE_SCHEMA },
)
if (r && r.blocked) log(`${TRACK.label}/${stage}: bị chặn bởi gate — ${r.gateWarning || 'xem result.gateWarning'}`)
return wrap('stage', r, { activities: acts, nextGate: def.gate, signersRequired: signersOf(def.gate) })