#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { buildMetrics, readCsv } = require('./review-metrics'); const { buildCustomerEffectSummary } = require('./customer-effect-audit'); function main() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-customer-effect-')); const reviewCsv = path.join(dir, 'reviewed.csv'); fs.writeFileSync(reviewCsv, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型,本轮人工补号量', 'brief-a,baseline-live,1,小红书,基础召回A,80,80,40,60,70,70,0,0,基础召回,需复核,http://example.com/a1,商务复核,客户选中,,3', 'brief-a,baseline-live,2,小红书,基础召回B,79,79,40,60,70,70,0,0,基础召回,调性偏泛,http://example.com/a2,跑偏,,召回关键词错,3', 'brief-a,baseline-live,3,小红书,基础召回C,78,78,40,60,70,70,0,0,基础召回,需复核,http://example.com/a3,商务复核,客户选中,,3', 'brief-a,reference-account,4,小红书,参考召回A,90,90,88,85,85,88,2,0,参考相似,需复核,http://example.com/a4,可直接发客户,客户选中,,3', 'brief-a,reference-account,5,小红书,参考召回B,89,89,87,85,85,88,2,0,参考相似,需复核,http://example.com/a5,商务复核,客户选中,,3', 'brief-a,homepage-evidence,6,小红书,主页证据A,88,88,82,90,85,88,2,0,主页匹配,需复核,http://example.com/a6,商务复核,客户选中,,3', 'brief-a,homepage-evidence,7,小红书,主页证据B,87,87,82,89,85,88,2,0,主页匹配,需复核,http://example.com/a7,商务复核,客户选中,,3', 'brief-a,video-enhanced,8,小红书,视频证据A,86,86,84,88,85,88,2,0,视频证据匹配,需复核,http://example.com/a8,商务复核,客户选中,,3', 'brief-a,video-enhanced,9,小红书,视频证据B,85,85,84,88,85,88,2,0,视频证据匹配,需复核,http://example.com/a9,商务复核,,,3', 'brief-a,result-first,10,小红书,结果优先A,84,84,80,86,84,86,2,0,综合匹配,需复核,http://example.com/a10,商务复核,客户选中,,3' ].join('\n')), 'utf8'); const rows = readCsv(reviewCsv); const reviewMetrics = buildMetrics({ inputPath: reviewCsv, rows }); const passingHistory = { acceptance: { readyForCustomerEffectProof: true }, items: [ { id: 'brief-a', manualSupplementBaseline: 6 } ] }; const passSummary = buildCustomerEffectSummary({ reviewPath: reviewCsv, reviewRows: rows, reviewMetrics, history: passingHistory, historyAuditPath: path.join(dir, 'historical-dataset-audit.json') }); assert(passSummary.customerSelectedRate === 0.8, 'customer selected rate should be 80%'); assert(passSummary.referenceCustomerSelectedRate === 0.8333, 'reference selected rate should be 83%'); assert(passSummary.manualSupplementReductionRate === 0.5, 'manual supplement reduction should be 50%'); assert(passSummary.acceptance.customerSelectedRate30Pass, '30% selected-rate gate should pass'); assert(passSummary.acceptance.customerSelectedRate50Pass, '50% selected-rate gate should pass'); assert(passSummary.acceptance.referenceCustomerSelectedRate40Pass, 'reference selected-rate gate should pass'); assert(passSummary.acceptance.manualSupplementReduction50Pass, 'manual supplement reduction gate should pass'); assert(passSummary.acceptance.overallPass, 'complete customer effect proof should pass'); const missingCurrentCsv = path.join(dir, 'missing-current.csv'); fs.writeFileSync(missingCurrentCsv, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型', 'brief-b,reference-account,1,小红书,参考召回B,90,90,88,85,85,88,2,0,参考相似,需复核,http://example.com/b1,可直接发客户,客户选中,' ].join('\n')), 'utf8'); const missingCurrentRows = readCsv(missingCurrentCsv); const missingCurrentSummary = buildCustomerEffectSummary({ reviewPath: missingCurrentCsv, reviewRows: missingCurrentRows, reviewMetrics: buildMetrics({ inputPath: missingCurrentCsv, rows: missingCurrentRows }), history: passingHistory, historyAuditPath: path.join(dir, 'historical-dataset-audit.json') }); assert(!missingCurrentSummary.acceptance.currentManualSupplementAvailable, 'missing current manual supplement count should be pending'); assert(!missingCurrentSummary.acceptance.overallPass, 'missing current manual supplement count should block overall pass'); const missingBaselineSummary = buildCustomerEffectSummary({ reviewPath: reviewCsv, reviewRows: rows, reviewMetrics, history: { acceptance: { readyForCustomerEffectProof: false }, items: [{ id: 'brief-a', manualSupplementBaseline: null }] }, historyAuditPath: path.join(dir, 'bad-history.json') }); assert(!missingBaselineSummary.acceptance.manualSupplementBaselineAvailable, 'missing baseline should be detected'); assert(!missingBaselineSummary.acceptance.historyReadyForCustomerEffectProof, 'bad history audit should block customer proof'); assert(!missingBaselineSummary.acceptance.overallPass, 'missing baseline should block overall pass'); console.log(JSON.stringify({ ok: true, output: dir, customerSelectedRate: passSummary.customerSelectedRate, referenceCustomerSelectedRate: passSummary.referenceCustomerSelectedRate, manualSupplementReductionRate: passSummary.manualSupplementReductionRate }, null, 2)); } function withBom(text) { return `\uFEFF${text}`; } function assert(condition, message) { if (!condition) throw new Error(message); } if (require.main === module) main();