verify-policy-library.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * 验证「惠企政策」判定与置顶逻辑。
  3. *
  4. * 验证什么:
  5. * 1. 惠企政策判定——优先用接口字段 is_policy_library,缺失时回退到标题匹配,
  6. * 标题匹配要能容忍《》全半角等差异
  7. * 2. 置顶——本轮匹配到的惠企政策排在最前,其余保持库内原顺序
  8. *
  9. * 怎么跑(在项目根目录):
  10. * npm run build:tools # 或手工:npx esbuild <入口> --bundle ...
  11. * node harness/tools/verify-policy-library.mjs
  12. *
  13. * 说明:本脚本直接引用 src 里的同一份实现(policy-library-utils.ts),
  14. * 不是复制一份逻辑——复制会漂移,测的就不是真代码了。
  15. */
  16. import { createRequire } from 'node:module';
  17. import {
  18. normPolicyName,
  19. buildLibraryNameSet,
  20. isPolicyLibraryItem,
  21. collectMatchedLibraryNames,
  22. pinMatchedFirst,
  23. buildPolicyDetailUrl,
  24. POLICY_DETAIL_URL_BASE,
  25. } from './_policy-library-utils.mjs';
  26. const require = createRequire(import.meta.url);
  27. const library = require('../../public/merchant-agent/policies_public.v1.json');
  28. let pass = 0;
  29. let fail = 0;
  30. const check = (name, cond, extra = '') => {
  31. if (cond) {
  32. pass++;
  33. console.log(' ok ' + name);
  34. } else {
  35. fail++;
  36. console.log(' FAIL ' + name + ' ' + extra);
  37. }
  38. };
  39. console.log('惠企政策库:' + library.length + ' 条');
  40. const nameSet = buildLibraryNameSet(library);
  41. console.log('去重后政策名:' + nameSet.size + ' 个\n');
  42. console.log('=== 1. 名称归一化 ===');
  43. check(
  44. '《》差异可忽略',
  45. normPolicyName('《上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施》') ===
  46. normPolicyName('上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施')
  47. );
  48. check('括号差异可忽略', normPolicyName('政策(试行)') === normPolicyName('政策(试行)'));
  49. check('空白差异可忽略', normPolicyName(' 政策 A ') === normPolicyName('政策A'));
  50. console.log('\n=== 2. 惠企政策判定 ===');
  51. const sampleName = String(library[0].name).replace(/[《》]/g, '');
  52. check(
  53. '标题能在库里找到 → 是惠企政策',
  54. isPolicyLibraryItem({ title: sampleName }, nameSet)
  55. );
  56. check(
  57. '标题带《》也能找到 → 是惠企政策',
  58. isPolicyLibraryItem({ title: '《' + sampleName + '》' }, nameSet)
  59. );
  60. check(
  61. '接口字段 is_policy_library=true → 是惠企政策',
  62. isPolicyLibraryItem({ title: '库里没有的名字', originalData: { is_policy_library: true } }, nameSet)
  63. );
  64. check(
  65. '库中没有且无字段 → 不是惠企政策',
  66. !isPolicyLibraryItem({ title: '这个名字肯定不在惠企政策库里' }, nameSet)
  67. );
  68. check('空标题 → 不是惠企政策', !isPolicyLibraryItem({ title: '' }, nameSet));
  69. console.log('\n=== 3. 收集本轮匹配到的惠企政策 ===');
  70. const cards = [
  71. { title: sampleName }, // 命中
  72. { title: '不在库里的政策' },
  73. { title: '库里没有但后端标记了', originalData: { is_policy_library: true } },
  74. ];
  75. const matched = collectMatchedLibraryNames(cards, nameSet);
  76. check('收集到 2 条命中', matched.size === 2, '实际 ' + matched.size);
  77. console.log('\n=== 4. 置顶排序 ===');
  78. const items = [
  79. { title: 'A-未命中' },
  80. { title: sampleName }, // 命中,应置顶
  81. { title: 'B-未命中' },
  82. { title: '库里没有但后端标记了', originalData: { is_policy_library: true } }, // 命中,应置顶
  83. { title: 'C-未命中' },
  84. ];
  85. const ordered = pinMatchedFirst(items, matched, (i) => i.title);
  86. check('命中项排在最前', ordered.slice(0, 2).every((i) => matched.has(normPolicyName(i.title))), JSON.stringify(ordered.map((i) => i.title)));
  87. check('未命中项保持原顺序', JSON.stringify(ordered.slice(2).map((i) => i.title)) === JSON.stringify(['A-未命中', 'B-未命中', 'C-未命中']), JSON.stringify(ordered.slice(2).map((i) => i.title)));
  88. check('总数不变', ordered.length === items.length);
  89. check('无命中时顺序不变', JSON.stringify(pinMatchedFirst(items, new Set(), (i) => i.title).map((i) => i.title)) === JSON.stringify(items.map((i) => i.title)));
  90. console.log('\n=== 5. 真实场景抽样(库里前 3 条政策名)===');
  91. library.slice(0, 3).forEach((p) => {
  92. const t = String(p.name).replace(/[《》]/g, '');
  93. console.log(' ' + (isPolicyLibraryItem({ title: t }, nameSet) ? '✓' : '✗') + ' ' + t.slice(0, 40));
  94. });
  95. console.log('\n=== 6. 政策详情页地址(由 市级政策id 拼)===');
  96. {
  97. const withId = { data: { '市级政策id': '676eb30861aee302a8607104' } };
  98. check(
  99. '有 id → 拼出正确地址',
  100. buildPolicyDetailUrl(withId) === POLICY_DETAIL_URL_BASE + '?id=676eb30861aee302a8607104',
  101. buildPolicyDetailUrl(withId)
  102. );
  103. check('无 id → 空串(不猜)', buildPolicyDetailUrl({ data: {} }) === '');
  104. check('data 缺失 → 空串', buildPolicyDetailUrl({}) === '');
  105. check('null → 空串', buildPolicyDetailUrl(null) === '');
  106. check('id 为空白 → 空串', buildPolicyDetailUrl({ data: { '市级政策id': ' ' } }) === '');
  107. check('id 前后空格 → 去掉', buildPolicyDetailUrl({ data: { '市级政策id': ' abc ' } }) === POLICY_DETAIL_URL_BASE + '?id=abc');
  108. check('特殊字符 → 编码', buildPolicyDetailUrl({ data: { '市级政策id': 'a&b=c' } }) === POLICY_DETAIL_URL_BASE + '?id=a%26b%3Dc');
  109. // 真实库覆盖率
  110. const covered = library.filter((p) => buildPolicyDetailUrl(p)).length;
  111. console.log(' 真实库覆盖率: ' + covered + ' / ' + library.length);
  112. check('覆盖 261/262', covered === 261, '实际 ' + covered);
  113. const sample = library.find((p) => buildPolicyDetailUrl(p));
  114. console.log(' 示例: ' + buildPolicyDetailUrl(sample));
  115. }
  116. console.log('\n通过 ' + pass + ' 项,失败 ' + fail + ' 项');
  117. process.exit(fail ? 1 : 0);