#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const root = path.resolve(__dirname, '..'); function main() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-business-proof-progress-')); const outputs = path.join(tmp, 'outputs'); fs.mkdirSync(outputs, { recursive: true }); makeDir(path.join(outputs, 'data-intake-pack-latest'), { 'history-data-template.csv': 'brief编号,客户原始Brief\n', 'manual-review-template.csv': 'brief编号,人工复核标签\n', 'README.md': '# 数据收集包\n' }); makeDir(path.join(outputs, 'video-intake-pack-latest'), { 'video-resource-template.csv': 'brief编号,视频链接\n', 'README.md': '# 视频资源收集包\n' }); writeJson(path.join(outputs, 'intake-readiness-latest', 'intake-readiness-summary.json'), { failureCount: 9, acceptance: { historyReady: false, videoReady: false, reviewMetricsReady: false, customerEffectReady: false, overallReady: false }, history: { issues: [{ type: 'placeholder' }] } }); writeJson(path.join(outputs, 'video-resource-readiness-latest', 'video-resource-readiness-summary.json'), { failureCount: 1, counts: { realCandidateRows: 0 }, acceptance: { readyForVideoAbPreflight: false } }); writeJson(path.join(outputs, 'long-run-readiness-latest', 'long-run-readiness-summary.json'), { ready: false, counts: { pass: 2, fail: 5 } }); writeJson(path.join(outputs, 'proof-gap-closure-latest', 'proof-gap-closure-summary.json'), { complete: false, openCount: 4, rows: [ { id: 'video-ab-live-proof', status: 'closed', evidence: 'passed=true, strongDelta=5, evidenceDelta=20' } ] }); writeJson(path.join(outputs, 'historical-dataset-thin', 'historical-dataset-audit.json'), { briefCount: 5, categoryCount: 3, withCustomerDecision: 5, acceptance: { readyForLongRun: true, readyForCustomerEffectProof: true } }); const operatorOwnerArtifacts = [ { owner: '商务', markdown: 'outputs/proof-gap-operator-pack-latest/by-owner/business.md', csv: 'outputs/proof-gap-operator-pack-latest/by-owner/business.csv', actionCount: 2, topPriority: 1, gapIds: ['historical-dataset', 'manual-review-and-customer-effect'], evidenceRowCount: 2, nextActionCount: 2 }, { owner: '商务/投放', markdown: 'outputs/proof-gap-operator-pack-latest/by-owner/business-media.md', csv: 'outputs/proof-gap-operator-pack-latest/by-owner/business-media.csv', actionCount: 1, topPriority: 2, gapIds: ['video-real-candidate'], evidenceRowCount: 1, nextActionCount: 1 }, { owner: '技术/AI', markdown: 'outputs/proof-gap-operator-pack-latest/by-owner/tech-ai.md', csv: 'outputs/proof-gap-operator-pack-latest/by-owner/tech-ai.csv', actionCount: 2, topPriority: 3, gapIds: ['video-ab-live-proof', 'live-provider-overnight-proof'], evidenceRowCount: 2, nextActionCount: 2 } ]; writeJson(path.join(outputs, 'proof-gap-operator-pack-latest', 'proof-gap-operator-pack-summary.json'), { passed: true, complete: false, ownerArtifactCount: 3, ownerArtifacts: operatorOwnerArtifacts }); writeJson(path.join(outputs, 'business-execution-index-latest', 'business-execution-index-summary.json'), { passed: true, operatorPackOwnerArtifactCount: 3, operatorPackOwnerArtifactRowCount: 3, operatorPackOwnerArtifacts: operatorOwnerArtifacts }); writeJson(path.join(outputs, 'optimization-handoff-latest', 'optimization-handoff-summary.json'), { complete: false, statusCounts: { passed: 25, ready_not_proven: 2, blocked_by_external_data: 2 } }); writeJson(path.join(outputs, 'evidence-index-latest', 'evidence-index-summary.json'), { counts: { real_evidence: 5, smoke_or_local: 23, not_business_proof: 102 } }); writeJson(path.join(outputs, 'round-deposition-latest', 'round-deposition-summary.json'), { passed: true, counts: { pass: 40, fail: 0, warn: 0 } }); writeJson(path.join(outputs, 'optimization-status-latest', 'optimization-status-summary.json'), { complete: false, counts: { passed: 27, ready_not_proven: 2, blocked_by_external_data: 2 }, items: [ { id: 'video-ab-proof', status: 'ready_not_proven', evidence: 'acceptance:video-ab smoke is ready but live proof is missing' } ] }); const outputDir = path.join(outputs, 'business-proof-progress-latest'); const result = spawnSync(process.execPath, [ path.join(root, 'scripts', 'business-proof-progress.js'), '--outputs', outputs, '--output', outputDir ], { cwd: root, encoding: 'utf8' }); if (result.status !== 0) { process.stderr.write(result.stderr || result.stdout || ''); throw new Error('business-proof-progress should run'); } const summary = JSON.parse(fs.readFileSync(path.join(outputDir, 'business-proof-progress-summary.json'), 'utf8')); const report = fs.readFileSync(path.join(outputDir, 'business-proof-progress-report.md'), 'utf8').replace(/^\uFEFF/, ''); assert(summary.complete === false, 'progress must not report complete with blocked proof data'); assert(summary.rows.length === 15, 'progress should include 15 steps'); assert(summary.counts.pass === 4, 'template packs, history import presence, and deposition refresh should pass in fixture'); assert(summary.counts.fail === 2, 'intake preaudit and longrun gate should fail in fixture'); assert(summary.counts.blocked_by_external_data === 7, 'external data blockers should be explicit'); assert(summary.counts.pending === 2, 'pipeline and unproven video A/B should remain pending'); assert(summary.rows.some(row => row.order === 6 && row.status === 'pass'), 'history import should pass when audit file exists'); assert(summary.rows.some(row => row.order === 7 && row.status === 'blocked_by_external_data' && row.evidence.includes('missing=')), 'thin history audit should stay blocked and expose missing details'); assert(summary.rows.some(row => row.title === '客户效果审计' && row.status === 'blocked_by_external_data'), 'customer effect should be blocked by real data'); assert(summary.rows.some(row => row.title === '视频 A/B 验收' && row.status === 'pending' && row.evidence.includes('ready_not_proven')), 'video A/B should stay pending when status audit is ready_not_proven'); assert(summary.rows.every(row => row.owner !== '投放/商务'), 'progress owner labels should be normalized'); assert(summary.observed.videoAbProofStatus === 'ready_not_proven', 'progress should expose video A/B status audit result'); assert(summary.operatorPackOwnerArtifactCount === 3, 'progress should expose proof-gap operator owner attachment count'); assert(Array.isArray(summary.operatorPackOwnerArtifacts) && summary.operatorPackOwnerArtifacts.length === 3, 'progress should carry proof-gap operator owner attachments'); assert(summary.operatorPackOwnerArtifacts.some(item => item.markdown.includes('by-owner/business.md') && item.csv.includes('by-owner/business.csv')), 'progress should carry business owner operator attachment'); assert(summary.observed.businessExecutionOperatorPackOwnerArtifactRowCount === 3, 'progress should expose business execution operator attachment rows'); assert(report.includes('# 提号补证 15 步进度表'), 'report should be Chinese'); assert(report.includes('blocked_by_external_data'), 'report should show blocked status'); assert(report.includes('by-owner/business-media.md'), 'report should show proof-gap operator owner attachments'); assert(report.includes('不能把 sample、smoke'), 'report should preserve proof boundary'); assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns'); assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns'); console.log(JSON.stringify({ ok: true, outputDir, complete: summary.complete, counts: summary.counts }, null, 2)); } function makeDir(dir, files) { fs.mkdirSync(dir, { recursive: true }); for (const [name, content] of Object.entries(files)) { fs.writeFileSync(path.join(dir, name), content, 'utf8'); } } function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value, null, 2), 'utf8'); } function assert(condition, message) { if (!condition) throw new Error(message); } main();