#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { buildMetrics, readCsv } = require('./review-metrics'); function main() { const out = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-review-metrics-')); const csvPath = path.join(out, 'reviewed.csv'); fs.writeFileSync(csvPath, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型', 'brief-a,result-first,1,小红书,敏感肌成分研究所,87,100,50,89,70,95,0,0,匹配敏感肌,需确认报价,http://example.com/a,可直接发客户,客户选中', 'brief-a,result-first,2,小红书,真实护肤记录,82,90,60,80,75,83,0,0,匹配真实体验,需确认档期,http://example.com/b,商务复核,', 'brief-a,result-first,3,小红书,泛生活方式号,62,50,40,30,45,41,0,0,泛生活方式,调性偏泛,http://example.com/c,跑偏,,需求解析错', 'brief-a,result-first,4,抖音,精致护肤测评,80,88,70,75,76,79,0,0,测评结构清楚,需补主页近作,http://example.com/d,商务复核,' ].join('\n')), 'utf8'); const metrics = buildMetrics({ inputPath: csvPath, rows: readCsv(csvPath) }); assert(metrics.headerOk, 'header should accept software table plus customer selection field'); assert(metrics.duplicateKeyCount === 0, 'duplicate keys should be zero'); assert(metrics.rankContinuous, 'ranks should be continuous'); assert(metrics.businessUsableRate === 0.75, 'business usable rate should be 75%'); assert(metrics.offTargetHardFailRate === 0.25, 'negative rate should be 25%'); assert(metrics.customerSelectedRate === 0.25, 'customer selected rate should be 25%'); assert(metrics.failureAttribution.failureRows === 1, 'one negative row should be counted'); assert(metrics.failureAttribution.attributedCount === 1, 'negative row should have attribution'); assert(metrics.acceptance.failureAttributionCoveragePass, 'attributed negative rows should pass attribution gate'); assert(metrics.byStrategy.length === 1, 'strategy summary should include result-first'); assert(metrics.strategyComparison.reference.total === 0, 'reference comparison should be pending without reference strategies'); assert(metrics.acceptance.referencePassRateHigherThanKeyword === null, 'reference vs keyword gate should be pending without comparison groups'); assert(!metrics.acceptance.overallPass, 'sample should fail because negative rate is above gate'); const missingAttributionPath = path.join(out, 'missing-attribution.csv'); fs.writeFileSync(missingAttributionPath, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,归因类型', 'brief-b,result-first,1,小红书,不匹配账号,60,50,40,30,45,41,0,0,泛生活方式,调性偏泛,http://example.com/e,调性不符,' ].join('\n')), 'utf8'); const missingMetrics = buildMetrics({ inputPath: missingAttributionPath, rows: readCsv(missingAttributionPath) }); assert(missingMetrics.failureAttribution.failureRows === 1, 'tone mismatch should be treated as negative'); assert(missingMetrics.failureAttribution.missingAttributionCount === 1, 'missing attribution should be counted'); assert(!missingMetrics.acceptance.failureAttributionCoveragePass, 'missing attribution should fail attribution gate'); const strategyComparePath = path.join(out, 'strategy-compare.csv'); fs.writeFileSync(strategyComparePath, withBom([ 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签,客户选择,归因类型', 'brief-c,baseline-live,1,小红书,基础召回A,80,80,40,50,70,70,0,0,基础召回,需复核,http://example.com/f,商务复核,客户选中,', 'brief-c,baseline-live,2,小红书,基础召回B,78,78,40,50,70,70,0,0,基础召回,需复核,http://example.com/g,跑偏,,召回关键词错', 'brief-c,reference-account,3,小红书,参考召回A,90,90,85,80,80,85,2,0,参考账号相似,需复核,http://example.com/h,可直接发客户,客户选中,', 'brief-c,reference-account,4,小红书,参考召回B,88,88,82,78,80,85,2,0,参考账号相似,需复核,http://example.com/i,商务复核,客户选中,', 'brief-c,homepage-evidence,5,小红书,主页证据A,86,86,80,88,80,85,2,0,主页近作匹配,需复核,http://example.com/j,商务复核,,' ].join('\n')), 'utf8'); const strategyMetrics = buildMetrics({ inputPath: strategyComparePath, rows: readCsv(strategyComparePath) }); assert(strategyMetrics.strategyComparison.keyword.businessUsableRate === 0.5, 'keyword baseline usable rate should be 50%'); assert(strategyMetrics.strategyComparison.reference.businessUsableRate === 1, 'reference usable rate should be 100%'); assert(strategyMetrics.acceptance.referencePassRateHigherThanKeyword === true, 'reference strategies should outperform keyword baseline'); assert(strategyMetrics.strategyComparison.reference.customerSelectedRate >= 0.4, 'reference customer selected rate should meet 40% gate'); assert(strategyMetrics.acceptance.referenceCustomerSelectedRatePass === true, 'reference selected-rate gate should pass when measured'); console.log(`review metrics smoke ok: ${out}`); } function withBom(text) { return `\uFEFF${text}`; } function assert(condition, message) { if (!condition) throw new Error(message); } if (require.main === module) main();