export const meta = { name: 'generate-proposal', description: 'Tổng hợp tài liệu SAD thành proposal khách hàng dạng HTML/CSS: content (nội dung thương mại) → build (HTML) → review (rò rỉ nội bộ, trung thực, chất lượng HTML). Mỗi lần gọi chạy 1 stage (args.stage) có gate; "all" chạy liền kèm 1 vòng tự sửa theo review.', whenToUse: 'Gọi từ skill sad-proposal sau khi SAD đã hoàn thiện (docs/SAD.md) và đã có docs/proposal/proposal-config.md.', phases: [ { title: 'Content', detail: 'proposal-writer: SAD + config → proposal-content.md' }, { title: 'Build', detail: 'proposal-builder: content → index.html + artifact.html' }, { title: 'Review', detail: 'proposal-reviewer: rò rỉ nội bộ, trung thực, chất lượng HTML' }, ], } // --------------------------------------------------------------------------- // Tham số // stage: content | build | review | all (mặc định: all) // notes: ghi chú người duyệt khi chạy lại 1 stage (Revise) // keyFacts: danh sách {fact, source} từ gate content, truyền cho stage review // autoFix: (all) tự sửa 1 vòng theo review — mặc định true // sadFile / sectionsDir / briefFile / configFile / outDir: đường dẫn // --------------------------------------------------------------------------- const a = args && typeof args === 'object' ? args : {} const STAGES = ['content', 'build', 'review'] 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 SAD = a.sadFile || 'docs/SAD.md' const S = a.sectionsDir || 'docs/sections' const BRIEF = a.briefFile || 'docs/00-project-brief.md' const CFG = a.configFile || 'docs/proposal/proposal-config.md' const OUT = a.outDir || 'docs/proposal' const CONTENT = `${OUT}/proposal-content.md` const HTML = `${OUT}/index.html` const ART = `${OUT}/artifact.html` const autoFix = a.autoFix !== false 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` : '' const run = (s) => stage === 'all' || stage === s // --------------------------------------------------------------------------- // Schema — mọi agent trả structured output để script rẽ nhánh bằng code // --------------------------------------------------------------------------- const CONTENT_SCHEMA = { type: 'object', properties: { filesWritten: { type: 'array', items: { type: 'string' } }, sections: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, sourceSadSections: { type: 'array', items: { type: 'string' } } }, required: ['id', 'title'], }, }, placeholders: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, description: { type: 'string' }, section: { type: 'string' } }, required: ['description'], }, }, excludedInternal: { type: 'array', items: { type: 'string' } }, keyFacts: { type: 'array', items: { type: 'object', properties: { fact: { type: 'string' }, source: { type: 'string' } }, required: ['fact', 'source'] }, }, confidence: { type: 'string', enum: ['high', 'medium', 'low'] }, summary: { type: 'string' }, }, required: ['filesWritten', 'sections', 'placeholders', 'confidence', 'summary'], } const BUILD_SCHEMA = { type: 'object', properties: { filesWritten: { type: 'array', items: { type: 'string' } }, sectionsRendered: { type: 'array', items: { type: 'string' } }, placeholdersCount: { type: 'number' }, mermaidBlocks: { type: 'number' }, approxSizeKB: { type: 'number' }, checks: { type: 'object', properties: { standaloneDoc: { type: 'boolean' }, artifactVariant: { type: 'boolean' }, title: { type: 'boolean' }, tocAnchors: { type: 'boolean' }, themeTokens: { type: 'boolean' }, printCss: { type: 'boolean' }, responsiveTables: { type: 'boolean' }, mermaidLoaderGuarded: { type: 'boolean' }, }, }, confidence: { type: 'string', enum: ['high', 'medium', 'low'] }, summary: { type: 'string' }, }, required: ['filesWritten', 'checks', 'confidence', 'summary'], } const REVIEW_SCHEMA = { type: 'object', properties: { verdict: { type: 'string', enum: ['pass', 'revise'] }, leaks: { type: 'array', items: { type: 'object', properties: { file: { type: 'string' }, snippet: { type: 'string' }, why: { type: 'string' } }, required: ['file', 'snippet'] }, }, placeholders: { type: 'array', items: { type: 'string' } }, findings: { type: 'array', items: { type: 'object', properties: { target: { type: 'string', enum: ['content', 'html'] }, issue: { type: 'string' }, suggestion: { type: 'string' }, severity: { type: 'string', enum: ['high', 'medium', 'low'] }, }, required: ['target', 'issue', 'severity'], }, }, factChecks: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, sadEvidence: { type: 'string' }, ok: { type: 'boolean' } }, required: ['claim', 'ok'] }, }, summary: { type: 'string' }, }, required: ['verdict', 'leaks', 'findings', 'summary'], } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const sources = `Nguồn: ${CFG} (thông tin thương mại — nguồn duy nhất cho giá/ngày/tên người), ` + `${SAD} (nếu chưa có thì đọc ${BRIEF} + toàn bộ ${S}/01–09).` const fmtFindings = (list) => list.map((f, i) => `${i + 1}. [${f.severity}] ${f.issue}${f.suggestion ? ` → ${f.suggestion}` : ''}`).join('\n') function writeContent(extra) { return agent( `${sources}\nSoạn nội dung proposal khách hàng theo đúng cấu trúc và marker trong system prompt của bạn, ghi ra ${CONTENT}. ` + `Tuyệt đối không đưa nội dung nội bộ (ghi chú rà soát, trạng thái duyệt, câu hỏi mở, findings, mã FR ngoài Phụ lục) vào proposal. ` + `Không bịa giá/ngày/số liệu — dùng [[CẦN ĐIỀN: ...]] và liệt kê trong placeholders.${extra || ''}`, { agentType: 'proposal-writer', label: 'proposal-writer', phase: 'Content', schema: CONTENT_SCHEMA }, ) } function buildHtml(extra) { return agent( `Đọc ${CONTENT} và ${CFG} (brandColor, language, logo nếu có). Dựng ${HTML} (tài liệu HTML độc lập hoàn chỉnh) ` + `và ${ART} (biến thể Artifact: không doctype/html/head/body, bắt đầu bằng rồi <style>) theo đúng design spec trong system prompt. ` + `Không đổi câu chữ của content; mọi [[CẦN ĐIỀN]] phải thành <mark class="todo">. Tự kiểm checklist trước khi trả kết quả.${extra || ''}`, { agentType: 'proposal-builder', label: 'proposal-builder', phase: 'Build', schema: BUILD_SCHEMA }, ) } function reviewAll(keyFacts) { const facts = Array.isArray(keyFacts) && keyFacts.length ? `\nkeyFacts từ proposal-writer cần đối chiếu từng mục:\n${keyFacts.map((k) => `- ${k.fact} (nguồn: ${k.source})`).join('\n')}` : '' return agent( `Rà soát ${CONTENT}, ${HTML}, ${ART} đối chiếu với ${SAD} (hoặc ${BRIEF} + ${S}/) và ${CFG}. ` + `Ba lăng kính: (1) rò rỉ nội dung nội bộ; (2) độ trung thực số liệu/cam kết; (3) placeholder còn lại và chất lượng HTML theo checklist. ` + `Không sửa file — chỉ trả findings.${facts}`, { agentType: 'proposal-reviewer', label: 'proposal-reviewer', phase: 'Review', schema: REVIEW_SCHEMA }, ) } function contentGate(c) { const r = c || {} return { stage: 'content', ok: !!c, filesWritten: r.filesWritten || [], sections: r.sections || [], placeholders: r.placeholders || [], excludedInternal: r.excludedInternal || [], keyFacts: r.keyFacts || [], confidence: r.confidence || 'unknown', summary: r.summary || '', } } function buildGate(b) { const r = b || {} const checks = r.checks || {} const failed = Object.keys(checks).filter((k) => checks[k] === false) return { stage: 'build', ok: !!b, filesWritten: r.filesWritten || [], sectionsRendered: r.sectionsRendered || [], placeholdersCount: typeof r.placeholdersCount === 'number' ? r.placeholdersCount : null, mermaidBlocks: r.mermaidBlocks, approxSizeKB: r.approxSizeKB, checks, failedChecks: failed, confidence: r.confidence || 'unknown', summary: r.summary || '', files: { html: HTML, artifact: ART }, } } function reviewGate(v) { const r = v || {} const leaks = r.leaks || [] const placeholders = r.placeholders || [] const findings = r.findings || [] return { stage: 'review', ok: !!v, verdict: r.verdict || 'unknown', leaks, placeholders, findings, factChecks: r.factChecks || [], failedFacts: (r.factChecks || []).filter((f) => f.ok === false), canSend: r.verdict === 'pass' && leaks.length === 0 && placeholders.length === 0, summary: r.summary || '', } } // --------------------------------------------------------------------------- // Pipeline // --------------------------------------------------------------------------- const report = { stage, autoFixed: false, files: { content: CONTENT, html: HTML, artifact: ART } } if (stage === 'all') log('Chế độ "all": content → build → review, tự sửa tối đa 1 vòng theo review. Không có cổng phê duyệt giữa các bước.') if (run('content')) { phase('Content') const c = await writeContent(notes) report.content = contentGate(c) if (stage === 'content') return report.content } if (run('build')) { phase('Build') const b = await buildHtml(notes) report.build = buildGate(b) if (stage === 'build') return report.build } if (run('review')) { phase('Review') let keyFacts = report.content ? report.content.keyFacts : (Array.isArray(a.keyFacts) ? a.keyFacts : []) const v = await reviewAll(keyFacts) report.review = reviewGate(v) if (stage === 'review') return report.review // --- all: 1 vòng tự sửa --------------------------------------------------- if (autoFix && v && v.verdict === 'revise') { const fixable = (v.findings || []).filter((f) => f.severity !== 'low') const leakNotes = (v.leaks || []).map((l) => ({ severity: 'high', target: 'content', issue: `Rò rỉ nội dung nội bộ trong ${l.file}: "${l.snippet}"`, suggestion: l.why || 'Loại bỏ hoặc viết lại theo góc nhìn khách hàng', })) const contentNotes = fixable.filter((f) => f.target === 'content').concat(leakNotes) const htmlNotes = fixable.filter((f) => f.target === 'html') if (contentNotes.length) { log(`Tự sửa: ${contentNotes.length} finding nội dung → chạy lại content, build, review.`) phase('Content') const c2 = await writeContent(`\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(contentNotes)}\n`) report.content = contentGate(c2) keyFacts = report.content.keyFacts phase('Build') const b2 = await buildHtml( htmlNotes.length ? `\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(htmlNotes)}\n\nNội dung cũng đã được sửa — dựng lại từ content mới.` : '\n\nNội dung đã được sửa theo review — dựng lại HTML từ content mới.', ) report.build = buildGate(b2) } else if (htmlNotes.length) { log(`Tự sửa: ${htmlNotes.length} finding HTML → chạy lại build, review.`) phase('Build') const b2 = await buildHtml(`\n\n## Findings từ reviewer (bắt buộc xử lý)\n${fmtFindings(htmlNotes)}\n`) report.build = buildGate(b2) } if (contentNotes.length || htmlNotes.length) { phase('Review') const v2 = await reviewAll(keyFacts) report.review = reviewGate(v2) report.autoFixed = true } else { log('Review yêu cầu sửa nhưng chỉ có finding mức low — bỏ qua tự sửa, để người duyệt quyết định.') } } } return report