review-metrics-smoke.js 5.7 KB

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