Files
2026-09-22 13:46:36 +07:00

229 lines
15 KiB
JavaScript
Raw Permalink 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: 'sa-pipeline',
description: 'Điều phối quy trình Solution Architect (sa-1…sa-4) từng bước có người duyệt: mỗi lần gọi chạy đúng MỘT stage/activity (--focus), hoặc audit (chấm AG1–AG4 + DTM/ADL, chỉ đọc), sign (ghi chữ ký duyệt của con người), sync (INDEX/ADL). Không có chế độ chạy liền.',
whenToUse: 'Gọi từ skill sa-pipeline. Bắt buộc args.project, args.stage, args.date.',
phases: [
{ title: 'Init', detail: 'sa-lifecycle: cấu trúc sa-output + INDEX' },
{ title: 'Audit', detail: 'sa-gate-auditor: chấm AG1–AG4, Confidence, DTM/ADL, đồng bộ BA (chỉ đọc)' },
{ title: 'Context', detail: 'sa-1: driver, ràng buộc, as-is, phương án, TCO, rủi ro' },
{ title: 'Architecture', detail: 'sa-2: QAS, ASR, SAD, ICD, DAT, SEC, INF, FAIL, ADR + lớp bàn giao dev DOM (class), CTR (OpenAPI/AsyncAPI), PDM (schema vật lý + DDL/migration)' },
{ title: 'Enablement', detail: 'sa-3: gói bàn giao dev (HANDOFF), guideline, fitness function, design review, tech debt' },
{ title: 'Evolution', detail: 'sa-4: conformance, chi phí, drift, post-mortem, roadmap' },
{ title: 'Sign', detail: 'Ghi quyết định duyệt/baseline/revise của con người' },
{ title: 'Sync', detail: 'Cập nhật INDEX/ADL từ header thật' },
],
}
const TRACK = {
label: 'SA',
outputRoot: 'sa-output',
runner: 'sa-stage-runner',
auditor: 'sa-gate-auditor',
lifecycleSkill: 'sa-lifecycle',
traceSkill: 'sa-conformance',
indexDir: '00-index',
stages: {
context: { phase: 'Context', skill: 'sa-1-context', dir: '01-context', gate: 'AG1', prevGate: null,
activities: ['drivers', 'constraints', 'as-is', 'options', 'cost-risk'], externalPrereq: 'BA đã qua G1 (có BRIEF/GOAL/RQ)' },
architecture: { phase: 'Architecture', skill: 'sa-2-architecture', dir: '02-architecture', gate: 'AG2', prevGate: 'AG1',
activities: ['qas', 'asr', 'sad', 'icd', 'dat', 'sec', 'inf', 'fail', 'adr', 'dom', 'ctr', 'pdm'], orderedPrefix: ['qas', 'asr', 'sad'],
scopeHint: 'dom cần SAD+DAT · ctr cần ICD (sinh contracts/*.yaml) · pdm cần DAT+DOM (sinh schema/*.sql); sơ đồ theo chuẩn Archify (diagrams/*.json)' },
enablement: { phase: 'Enablement', skill: 'sa-3-enablement', dir: '03-enablement', gate: 'AG3', prevGate: 'AG2',
activities: ['handoff', 'agd', 'fit', 'review', 'debt'], scopeHint: 'handoff: phạm vi bàn giao (US/module/release)' },
evolution: { phase: 'Evolution', skill: 'sa-4-evolution', dir: '04-evolution', gate: 'AG4', prevGate: 'AG3',
activities: ['conformance', 'cost', 'drift', 'postmortem', 'review', 'roadmap'] },
},
gates: {
AG1: 'PO + Tech Lead', AG2: 'Tech Lead + Security + Ops/SRE', AG3: 'Tech Lead + QA + SRE', AG4: 'PO + SRE + Enterprise Architect',
},
profileFields: [],
}
// ===========================================================================
// 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)
// 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.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) })