245 lines
17 KiB
JavaScript
245 lines
17 KiB
JavaScript
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) {
|
||
const ans = typeof a.answers === 'string'
|
||
? a.answers
|
||
: Object.entries(a.answers).map(([k, v]) => `- **${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`).join('\n')
|
||
lines.push(`## Câu trả lời của người dùng cho OQ / humanInputNeeded vòng trước\n${ans}`)
|
||
}
|
||
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) })
|