#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const OUTPUTS = path.join(ROOT, 'outputs'); function main() { const args = parseArgs(process.argv.slice(2)); const outputDir = path.resolve(args.output || process.env.TIHAO_BUSINESS_PROOF_PROGRESS_OUTPUT || path.join(OUTPUTS, `business-proof-progress-${Date.now()}`)); const summary = buildBusinessProofProgress({ root: ROOT, outputsDir: path.resolve(args.outputs || OUTPUTS) }); fs.mkdirSync(outputDir, { recursive: true }); const jsonPath = path.join(outputDir, 'business-proof-progress-summary.json'); const reportPath = path.join(outputDir, 'business-proof-progress-report.md'); fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8'); console.log(JSON.stringify({ outputDir, json: jsonPath, report: reportPath, complete: summary.complete, pass: summary.counts.pass, blocked: summary.counts.blocked_by_external_data, pending: summary.counts.pending, fail: summary.counts.fail }, null, 2)); if (!summary.complete && args.strict) process.exitCode = 1; } function buildBusinessProofProgress({ root, outputsDir }) { const dataPackDir = latestDir(outputsDir, 'data-intake-pack'); const videoPackDir = latestDir(outputsDir, 'video-intake-pack'); const intake = readJson(latestFile(outputsDir, 'intake-readiness-summary.json')); const videoReadiness = readJson(latestFile(outputsDir, 'video-resource-readiness-summary.json')); const longrun = readJson(latestFile(outputsDir, 'long-run-readiness-summary.json')); const closure = readJson(latestFile(outputsDir, 'proof-gap-closure-summary.json')); const handoff = readJson(latestFile(outputsDir, 'optimization-handoff-summary.json')); const evidence = readJson(latestFile(outputsDir, 'evidence-index-summary.json')); const round = readJson(latestFile(outputsDir, 'round-deposition-summary.json')); const status = readJson(latestFile(outputsDir, 'optimization-status-summary.json')); const history = readJson(latestFile(outputsDir, 'historical-dataset-audit.json')); const review = readJson(latestFile(outputsDir, 'review-metrics-summary.json')); const customerEffect = readJson(latestFile(outputsDir, 'customer-effect-summary.json')); const pipeline = readJson(latestFile(outputsDir, 'optimization-pipeline-summary.json')); const proofGapOperatorPack = readJson(latestFile(outputsDir, 'proof-gap-operator-pack-summary.json')); const businessExecutionIndex = readJson(latestFile(outputsDir, 'business-execution-index-summary.json')); const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts( proofGapOperatorPack?.ownerArtifacts || businessExecutionIndex?.operatorPackOwnerArtifacts || [] ); const operatorPackOwnerArtifactCount = Number(proofGapOperatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0); const closureRow = (id) => Array.isArray(closure?.rows) ? closure.rows.find(row => row.id === id) : null; const intakeOverallReady = intake?.acceptance?.overallReady === true && Number(intake?.failureCount || 0) === 0; const videoReady = videoReadiness?.acceptance?.readyForVideoAbPreflight === true && Number(videoReadiness?.failureCount || 0) === 0; const historyMissingRequirements = getHistoryCustomerEffectProofMissingRequirements(history); const historyReady = historyMissingRequirements.length === 0; const reviewReady = review?.acceptance?.overallPass === true; const customerEffectReady = customerEffect?.acceptance?.overallPass === true; const videoAbStatus = statusItem(status, 'video-ab-proof'); const videoAbPassed = videoAbStatus?.status === 'passed' && closureRow('video-ab-live-proof')?.status === 'closed'; const videoAbEvidence = videoAbStatus ? `status=${videoAbStatus.status}, closure=${closureRow('video-ab-live-proof')?.status || 'missing'}, evidence=${videoAbStatus.evidence || ''}` : (closureRow('video-ab-live-proof')?.evidence || 'missing video-ab status row'); const rows = [ step(1, '商务', '生成统一收集包', dataPackDir ? 'pass' : 'pending', dataPackDir ? rel(root, dataPackDir) : 'missing data-intake-pack', '没有统一模板时先运行 data:intake-template。'), step(2, '商务/投放', '生成视频资源收集包', videoPackDir ? 'pass' : 'pending', videoPackDir ? rel(root, videoPackDir) : 'missing video-intake-pack', '没有视频模板时先运行 video:intake-template。'), step(3, '商务', '填真实历史 Brief', intake?.acceptance?.historyReady ? 'pass' : 'blocked_by_external_data', intake ? `historyReady=${Boolean(intake.acceptance?.historyReady)}, issues=${intake.history?.issues?.length || 0}` : 'missing intake-readiness', '补 5-10 个真实 Brief、人工名单、客户选择、拒绝原因和历史人工补号量基线。'), step(4, '商务/投放', '填真实视频资源', intake?.acceptance?.videoReady || videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `realCandidate=${videoReadiness.counts?.realCandidateRows || 0}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补真实参考视频和真实候选视频 URL、封面、ASR、帧图或正文证据。'), step(5, '技术/AI', '真实材料预审', intakeOverallReady ? 'pass' : 'fail', intake ? `overallReady=${Boolean(intake.acceptance?.overallReady)}, failureCount=${intake.failureCount || 0}` : 'missing intake-readiness', '先修 intake readiness 失败项,不能跳过预审。'), step(6, '技术/AI', '历史数据导入', history ? 'pass' : 'pending', history ? 'historical-dataset-audit.json present' : 'missing historical-dataset-audit.json', '真实历史 CSV 到位后运行 history:from-csv。'), step(7, '技术/AI', '历史数据审计', historyReady ? 'pass' : 'blocked_by_external_data', history ? `readyForCustomerEffectProof=${Boolean(history.acceptance?.readyForCustomerEffectProof)}, missing=${historyMissingRequirements.length}` : 'missing historical-dataset-audit.json', '运行 history:audit --strict,直到可支撑客户效果证明。'), step(8, '技术/AI', '视频资源就绪审计', videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `readyForVideoAbPreflight=${Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight)}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补齐真实候选视频后重跑 video:resource-readiness --strict。'), step(9, '技术/AI', '长跑前门禁', longrun?.ready ? 'pass' : 'fail', longrun ? `ready=${Boolean(longrun.ready)}, fail=${longrun.counts?.fail || 0}` : 'missing long-run-readiness', 'longrun:readiness ready=true 前不要启动 overnight。'), step(10, '技术/AI', '运行策略矩阵', pipeline?.readyForClaim ? 'pass' : 'pending', pipeline ? `readyForClaim=${Boolean(pipeline.readyForClaim)}, pass=${pipeline.counts?.pass || 0}, fail=${pipeline.counts?.fail || 0}` : 'missing optimization-pipeline-summary.json', '真实材料和长跑门禁通过后再跑 optimization:pipeline。'), step(11, '技术/AI', '视频 A/B 验收', videoAbPassed ? 'pass' : 'pending', videoAbEvidence, '只有真实视频 A/B 通过且 status 审计为 passed 时才可用于视频提升结论。'), step(12, '商务', '人工复核标注', intake?.acceptance?.reviewMetricsReady ? 'pass' : 'blocked_by_external_data', intake ? `reviewMetricsReady=${Boolean(intake.acceptance?.reviewMetricsReady)}, customerEffectReady=${Boolean(intake.acceptance?.customerEffectReady)}` : 'missing intake-readiness', '补商务复核标签、负样本归因、客户选择和本轮人工补号量。'), step(13, '技术/AI', '商务复核指标', reviewReady ? 'pass' : 'blocked_by_external_data', review ? `overallPass=${Boolean(review.acceptance?.overallPass)}` : 'missing review-metrics-summary.json', '已标注 CSV 到位后运行 review:metrics --strict。'), step(14, '技术/AI', '客户效果审计', customerEffectReady ? 'pass' : 'blocked_by_external_data', customerEffect ? `overallPass=${Boolean(customerEffect.acceptance?.overallPass)}` : 'missing customer-effect-summary.json', '客户选择、历史基线和本轮人工补号量到位后运行 customer-effect:audit --strict。'), step(15, '技术/AI', '刷新证据和交接', round?.passed && handoff && evidence ? 'pass' : 'pending', `round=${Boolean(round?.passed)}, handoff=${Boolean(handoff)}, evidence=${Boolean(evidence)}, operatorPackOwnerArtifactCount=${operatorPackOwnerArtifactCount}`, '每轮结束刷新 evidence:index、proof-gap:closure、handoff:summary 和 round:deposition。') ]; const counts = countRows(rows); return { generatedAt: new Date().toISOString(), root, outputsDir, complete: rows.every(row => row.status === 'pass'), counts, observed: { intakeReady: intakeOverallReady, intakeFailureCount: intake ? Number(intake.failureCount || 0) : null, videoReady, videoFailureCount: videoReadiness ? Number(videoReadiness.failureCount || 0) : null, longrunReady: Boolean(longrun?.ready), longrunFailCount: longrun ? Number(longrun.counts?.fail || 0) : null, proofGapOpenCount: closure ? Number(closure.openCount || 0) : null, handoffComplete: Boolean(handoff?.complete), videoAbProofStatus: videoAbStatus?.status || null, proofGapOperatorPackOwnerArtifactCount: operatorPackOwnerArtifactCount, businessExecutionOperatorPackOwnerArtifactCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactCount || 0), businessExecutionOperatorPackOwnerArtifactRowCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0), evidenceCounts: evidence?.counts || {} }, operatorPackOwnerArtifactCount, operatorPackOwnerArtifacts, guardrails: [ '本进度表只同步补证执行状态,不能把 sample、smoke、模板、provider fallback 或接口 200 计为客户效果证明。', 'blocked_by_external_data 表示需要真实历史 Brief、真实视频资源、商务复核或客户选择数据。', '只有全部 15 步为 pass,且 customer-effect:audit 与 proof-gap:closure 通过后,才可进入长期完成判断。', 'proof-gap by-owner attachments are dispatch files only; they do not prove customer effect.', '不得在任何输入、报告或日志中写入 sessionToken、Authorization、模型 token 或 npm token。' ], rows }; } function statusItem(status, id) { return Array.isArray(status?.items) ? status.items.find(item => item.id === id) : null; } function getHistoryCustomerEffectProofMissingRequirements(history) { if (!history) return ['missing historical-dataset-audit.json']; const missing = []; const acceptance = history.acceptance || {}; if (acceptance.readyForCustomerEffectProof !== true) missing.push('acceptance.readyForCustomerEffectProof=true'); if (Number(history.briefCount || 0) < 5) missing.push('briefCount>=5'); if (acceptance.parseOk !== true) missing.push('acceptance.parseOk=true'); if (acceptance.minBriefsMet !== true) missing.push('acceptance.minBriefsMet=true'); if (acceptance.allHaveBriefText !== true) missing.push('acceptance.allHaveBriefText=true'); if (acceptance.allHaveManualFinalList !== true) missing.push('acceptance.allHaveManualFinalList=true'); if (acceptance.allHaveCustomerDecision !== true) missing.push('acceptance.allHaveCustomerDecision=true'); if (acceptance.allHaveFeedbackReason !== true) missing.push('acceptance.allHaveFeedbackReason=true'); if (acceptance.allHaveManualSupplementBaseline !== true) missing.push('acceptance.allHaveManualSupplementBaseline=true'); if (acceptance.categoryCoverageMet !== true) missing.push('acceptance.categoryCoverageMet=true'); if (Number(history.missingCriticalCount || 0) !== 0) missing.push('missingCriticalCount=0'); if (!Array.isArray(history.items) || history.items.length < 5) missing.push('items.length>=5'); if (Number(history.withManualFinalList || 0) < 5) missing.push('withManualFinalList>=5'); if (Number(history.withCustomerDecision || 0) < 5) missing.push('withCustomerDecision>=5'); if (Number(history.withRejectionReason || 0) < 1) missing.push('withRejectionReason>=1'); if (Number(history.withManualSupplementBaseline || 0) < 5) missing.push('withManualSupplementBaseline>=5'); return uniqueStrings(missing); } function step(order, owner, title, status, evidence, next) { return { order, owner, title, status, evidence, next }; } function normalizeOperatorPackOwnerArtifacts(ownerArtifacts) { if (!Array.isArray(ownerArtifacts)) return []; return ownerArtifacts.map(item => ({ owner: item.owner || '', actionCount: Number(item.actionCount || 0), topPriority: Number(item.topPriority || 0), markdown: String(item.markdown || '').replace(/\\/g, '/'), csv: String(item.csv || '').replace(/\\/g, '/'), gapIds: Array.isArray(item.gapIds) ? item.gapIds : [], evidenceRowCount: Number(item.evidenceRowCount || 0), nextActionCount: Number(item.nextActionCount || 0) })); } function renderReport(summary) { const lines = [ '# 提号补证 15 步进度表', '', `- 生成时间:${summary.generatedAt}`, `- 是否完成:${summary.complete ? '是' : '否'}`, `- pass:${summary.counts.pass || 0}`, `- fail:${summary.counts.fail || 0}`, `- pending:${summary.counts.pending || 0}`, `- blocked_by_external_data:${summary.counts.blocked_by_external_data || 0}`, '', '## 当前观测', '', `- intakeReady:${summary.observed.intakeReady ? 'true' : 'false'}`, `- intakeFailureCount:${summary.observed.intakeFailureCount ?? 'unknown'}`, `- videoReady:${summary.observed.videoReady ? 'true' : 'false'}`, `- videoFailureCount:${summary.observed.videoFailureCount ?? 'unknown'}`, `- longrunReady:${summary.observed.longrunReady ? 'true' : 'false'}`, `- longrunFailCount:${summary.observed.longrunFailCount ?? 'unknown'}`, `- proofGapOpenCount:${summary.observed.proofGapOpenCount ?? 'unknown'}`, `- handoffComplete:${summary.observed.handoffComplete ? 'true' : 'false'}`, `- videoAbProofStatus:${summary.observed.videoAbProofStatus ?? 'unknown'}`, `- proofGapOperatorPackOwnerArtifactCount:${summary.observed.proofGapOperatorPackOwnerArtifactCount ?? 'unknown'}`, `- businessExecutionOperatorPackOwnerArtifactCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactCount ?? 'unknown'}`, `- businessExecutionOperatorPackOwnerArtifactRowCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactRowCount ?? 'unknown'}`, '', '## 15 步进度', '', '| 顺序 | 负责人 | 目标 | 状态 | 证据 | 下一步 |', '| --- | --- | --- | --- | --- | --- |', ...summary.rows.map(row => `| ${row.order} | ${escapeCell(row.owner)} | ${escapeCell(row.title)} | ${row.status} | ${escapeCell(row.evidence)} | ${escapeCell(row.next)} |`), '', '## proof-gap 操作包负责人附件', '', '| 负责人 | 动作数 | Markdown | CSV | 缺口 |', '| --- | ---: | --- | --- | --- |', ...summary.operatorPackOwnerArtifacts.map(item => `| ${escapeCell(item.owner)} | ${item.actionCount} | ${escapeCell(item.markdown)} | ${escapeCell(item.csv)} | ${escapeCell((item.gapIds || []).join(';'))} |`), '', '## 边界', '', ...summary.guardrails.map(item => `- ${item}`) ]; return lines.join('\n'); } function latestDir(outputsDir, prefix) { if (!fs.existsSync(outputsDir)) return ''; const dirs = fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix)) .map(entry => path.join(outputsDir, entry.name)) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs); return dirs[0] || ''; } function latestFile(outputsDir, fileName) { if (!fs.existsSync(outputsDir)) return ''; return fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => path.join(outputsDir, entry.name, fileName)) .filter(fileExists) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || ''; } function readJson(file) { if (!file || !fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; } } function fileExists(file) { return Boolean(file) && fs.existsSync(file); } function countRows(rows) { return rows.reduce((acc, row) => { acc[row.status] = (acc[row.status] || 0) + 1; return acc; }, {}); } function uniqueStrings(values) { return [...new Set((values || []).filter(Boolean).map(value => String(value)))]; } function parseArgs(argv) { const args = {}; for (let index = 0; index < argv.length; index += 1) { const raw = argv[index]; if (!raw.startsWith('--')) continue; const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; index += 1; } } return args; } function rel(root, file) { return path.relative(root, file).replace(/\\/g, '/'); } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' '); } function withBom(text) { return `\uFEFF${text}`; } if (require.main === module) main(); module.exports = { buildBusinessProofProgress, renderReport };