Files
sys-analysis-design/.claude/workflows/ba-pipeline.js
Leonard-ThindPad-P50 2c7bcde741 improve BA skill
2026-09-09 06:34:57 +07:00

229 lines
15 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 (≤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' },
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)', 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
// 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 (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) })