#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const { isClaimableProofGapClosure } = require('./optimization-pipeline'); function main() { const root = path.resolve(__dirname, '..'); assert(isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 5, businessGapCount: 5 }), 'complete proof-gap closure with all 5 business gaps should be claimable'); assert(!isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 5 }), 'thin proof-gap closure without businessGapCount should not be claimable'); assert(!isClaimableProofGapClosure({ complete: true, openCount: 0, closedCount: 3, businessGapCount: 5 }), 'proof-gap closure with fewer than 5 closed business gaps should not be claimable'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-optimization-pipeline-')); const historyCsv = path.join(dir, 'history.csv'); const reviewCsv = path.join(dir, 'review.csv'); const output = path.join(dir, 'pipeline-output'); const dataPack = path.join(dir, 'data-pack'); const videoPack = path.join(dir, 'video-pack'); writeHistoryCsv(historyCsv); writeReviewCsv(reviewCsv); fs.mkdirSync(dataPack, { recursive: true }); fs.mkdirSync(videoPack, { recursive: true }); writeHistoryCsv(path.join(dataPack, 'history-data-template.csv'), { realUrls: true }); writeReviewCsv(path.join(dataPack, 'manual-review-template.csv'), { realUrls: true }); writeVideoCsv(path.join(videoPack, 'video-resource-template.csv')); const dryRun = spawnSync(process.execPath, [ path.join(root, 'scripts', 'optimization-pipeline.js'), '--history-csv', historyCsv, '--review-csv', reviewCsv, '--history-min-briefs', '3', '--current-manual-supplement-count', '3', '--output', path.join(dir, 'dry-run'), '--dry-run' ], { cwd: root, encoding: 'utf8' }); assert(dryRun.status === 0, 'dry run should pass'); const drySummary = JSON.parse(fs.readFileSync(path.join(dir, 'dry-run', 'optimization-pipeline-summary.json'), 'utf8')); assert(drySummary.results.every(item => item.status === 'planned'), 'dry run should only plan steps'); assert(Array.isArray(drySummary.nextActions) && drySummary.nextActions.some(item => item.title.includes('确认输入后执行真实 pipeline')), 'dry run should include next action to run real pipeline'); const dryReport = fs.readFileSync(path.join(dir, 'dry-run', 'optimization-pipeline-report.md'), 'utf8').replace(/^\uFEFF/, ''); assert(dryReport.includes('## 下一步动作'), 'pipeline report should include next actions section'); const packDryRun = spawnSync(process.execPath, [ path.join(root, 'scripts', 'optimization-pipeline.js'), '--data-pack', dataPack, '--video-pack', videoPack, '--history-min-briefs', '3', '--current-manual-supplement-count', '3', '--output', path.join(dir, 'pack-dry-run'), '--dry-run' ], { cwd: root, encoding: 'utf8' }); assert(packDryRun.status === 0, 'data/video pack dry run should pass'); const packDrySummary = JSON.parse(fs.readFileSync(path.join(dir, 'pack-dry-run', 'optimization-pipeline-summary.json'), 'utf8')); assert(packDrySummary.results.some(item => item.id === 'intake-readiness' && item.command.includes('intake:readiness')), 'data/video pack pipeline should plan intake readiness'); assert(packDrySummary.results.some(item => item.id === 'video-resource-readiness' && item.command.includes('video:resource-readiness')), 'data/video pack pipeline should plan video resource readiness'); assert(packDrySummary.results.some(item => item.id === 'proof-gap-closure' && item.command.includes('proof-gap:closure')), 'data/video pack pipeline should plan proof gap closure'); assert(packDrySummary.results.some(item => item.command.includes('--intake-readiness')), 'longrun readiness should receive intake readiness summary'); assert(packDrySummary.nextActions.some(item => item.command.includes('optimization:pipeline') && !item.command.includes('--dry-run')), 'pack dry run should tell user to rerun without dry-run'); const run = spawnSync(process.execPath, [ path.join(root, 'scripts', 'optimization-pipeline.js'), '--history-csv', historyCsv, '--review-csv', reviewCsv, '--history-min-briefs', '3', '--current-manual-supplement-count', '3', '--output', output ], { cwd: root, encoding: 'utf8' }); if (run.status !== 0) { process.stderr.write(run.stderr || run.stdout || ''); throw new Error('optimization pipeline should pass with complete local data'); } const summary = JSON.parse(fs.readFileSync(path.join(output, 'optimization-pipeline-summary.json'), 'utf8')); assert(summary.results.some(item => item.id === 'history-from-csv' && item.status === 'pass'), 'pipeline should run history-from-csv'); assert(summary.results.some(item => item.id === 'history-audit' && item.status === 'pass'), 'pipeline should run history-audit'); assert(summary.results.some(item => item.id === 'review-metrics' && item.status === 'pass'), 'pipeline should run review-metrics'); assert(summary.results.some(item => item.id === 'customer-effect-audit' && item.status === 'pass'), 'pipeline should run customer-effect-audit'); assert(summary.results.some(item => item.id === 'proof-gap-closure' && item.status === 'pass'), 'pipeline should run proof-gap-closure'); assert(summary.readyForClaim === false, 'pipeline must not claim business effect while proof gaps remain open'); assert(summary.proofGapClosure && summary.proofGapClosure.complete === false, 'pipeline should expose incomplete proof gap closure'); assert(summary.proofGapClosure && summary.proofGapClosure.claimable === false, 'pipeline should expose non-claimable proof-gap closure state'); assert(summary.nextActions.some(item => item.step === 'proof-gap-closure'), 'pipeline should ask to close proof gaps when closure is incomplete'); assert(Array.isArray(summary.nextActions), 'pipeline should always output nextActions array'); assert(fs.existsSync(path.join(output, 'customer-effect-audit', 'customer-effect-summary.json')), 'pipeline should write customer effect summary'); assert(fs.existsSync(path.join(output, 'proof-gap-closure', 'proof-gap-closure-summary.json')), 'pipeline should write proof gap closure summary'); assert(fs.existsSync(path.join(output, 'evidence-index', 'evidence-index-summary.json')), 'pipeline should write evidence index'); console.log(JSON.stringify({ ok: true, output, steps: summary.results.length }, null, 2)); } function writeHistoryCsv(file, options = {}) { const url = options.realUrls ? 'https://www.xiaohongshu.com/user/profile' : 'https://example.com'; fs.writeFileSync(file, withBom([ 'brief编号,项目名称,类目,客户原始Brief,历史人工补号量基线,参考账号或视频,平台,博主名称,主页链接,人工复核标签,客户选择,拒绝原因', `brief-a,历史A,护肤,敏感肌修护真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-a,小红书,敏感肌成分党,${url}/a1,可直接发客户,客户选中,`, `brief-a,历史A,护肤,敏感肌修护真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-a,小红书,硬广导购号,${url}/a2,跑偏,客户拒绝,硬广感太强`, `brief-b,历史B,母婴,母婴DHA真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-b,小红书,营养师妈妈,${url}/b1,商务复核,客户选中,`, `brief-b,历史B,母婴,母婴DHA真实历史Brief,6,https://www.xiaohongshu.com/explore/ref-b,小红书,泛生活号,${url}/b2,跑偏,客户拒绝,人设太泛`, `brief-c,历史C,家清,家清真实历史Brief,6,https://www.douyin.com/user/ref-c,抖音,家清实验室,${url}/c1,可直接发客户,客户选中,`, `brief-c,历史C,家清,家清真实历史Brief,6,https://www.douyin.com/user/ref-c,抖音,装修设计号,${url}/c2,跑偏,客户拒绝,内容偏装修` ].join('\n')), 'utf8'); } function writeReviewCsv(file, options = {}) { const url = options.realUrls ? 'https://www.xiaohongshu.com/user/profile' : 'http://example.com'; fs.writeFileSync(file, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型,本轮人工补号量', `brief-a,baseline-live,1,小红书,基础A,80,80,40,60,70,70,0,0,基础召回,需复核,${url}/r1,商务复核,客户选中,,3`, `brief-a,baseline-live,2,小红书,基础B,70,70,40,60,70,70,0,0,基础召回,跑偏,${url}/r2,跑偏,,召回关键词错,3`, `brief-a,baseline-live,3,小红书,基础C,80,80,40,60,70,70,0,0,基础召回,需复核,${url}/r3,商务复核,客户选中,,3`, `brief-a,reference-account,4,小红书,参考A,90,90,88,85,85,88,2,0,参考相似,需复核,${url}/r4,可直接发客户,客户选中,,3`, `brief-a,reference-account,5,小红书,参考B,89,89,87,85,85,88,2,0,参考相似,需复核,${url}/r5,商务复核,客户选中,,3`, `brief-a,homepage-evidence,6,小红书,主页A,88,88,82,90,85,88,2,0,主页匹配,需复核,${url}/r6,商务复核,客户选中,,3`, `brief-a,homepage-evidence,7,小红书,主页B,87,87,82,89,85,88,2,0,主页匹配,需复核,${url}/r7,商务复核,客户选中,,3`, `brief-a,video-enhanced,8,小红书,视频A,86,86,84,88,85,88,2,0,视频匹配,需复核,${url}/r8,商务复核,客户选中,,3`, `brief-a,video-enhanced,9,小红书,视频B,85,85,84,88,85,88,2,0,视频匹配,需复核,${url}/r9,商务复核,,,3`, `brief-a,result-first,10,小红书,结果A,84,84,80,86,84,86,2,0,综合匹配,需复核,${url}/r10,商务复核,客户选中,,3` ].join('\n')), 'utf8'); } function writeVideoCsv(file) { fs.writeFileSync(file, withBom([ 'brief编号,项目名称,平台,资源角色,博主名称,主页链接,视频链接,封面链接,字幕或ASR文本,帧图链接,标题,发布时间,内容摘要,风格调性标签,画面人设场景信号,风险提示,来源接口或备注,是否真实资源', 'brief-a,历史A,小红书,参考视频,参考账号,https://www.xiaohongshu.com/user/profile/ref-a,https://www.xiaohongshu.com/explore/video-a,https://sns-img.example.cn/a.jpg,真实敏感肌修护参考视频字幕文本,https://sns-img.example.cn/a-frame.jpg,参考视频标题,2026-06-01,真实参考内容摘要,真实记录/成分党,近景护肤场景,无,人工补充,是', 'brief-a,历史A,小红书,候选视频,候选账号,https://www.xiaohongshu.com/user/profile/candidate-a,https://www.xiaohongshu.com/explore/video-b,https://sns-img.example.cn/b.jpg,真实候选视频字幕文本用于风格分析,https://sns-img.example.cn/b-frame.jpg,候选视频标题,2026-06-02,真实候选内容摘要,真实记录/测评,居家护肤场景,无,人工补充,是' ].join('\n')), 'utf8'); } function withBom(text) { return `\uFEFF${text}`; } function assert(condition, message) { if (!condition) throw new Error(message); } if (require.main === module) main();