verify-policy-card-fields.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * 验证适配层产出的政策卡片字段:**同政策的多个申报事项必须能区分开**。
  3. *
  4. * 背景(2026-09-17 用户报的 bug):
  5. * 适配层原先把卡片的 `title` 与 `declaration_item` 都取自 `card.name.text`(只有政策名),
  6. * 而后端不下发 `id`/`policy_id`(实测恒为 undefined)——同政策的多个申报事项
  7. * 在字段上完全无法区分:详情面板标题一样、「查看详情」还会跳到同一条。
  8. *
  9. * 验证的不变量:
  10. * ① `declaration_item` 取后端每条唯一的 `data.title`(含申报事项后缀),同政策两条必须不同
  11. * ② 卡片标题 `title` 仍是政策名(`card.name.text`)—— 卡片区的观感不变
  12. * ③ 后端没给后缀时(单事项政策),`declaration_item` 退化为政策名,不出现空值
  13. *
  14. * 数据来源:`probe-policy-cards.mjs` 抓到的真实载荷形状(这里用合成数据,保证可重复;
  15. * 真实载荷每次问法不同、LLM 输出会变,不适合做断言基准)。
  16. *
  17. * 怎么跑(在项目根目录):
  18. * npx esbuild harness/tools/_entry-coordinator.ts --bundle --format=esm \
  19. * --outfile=harness/tools/_coordinator.mjs --alias:@=./src --define:import.meta.env='{}'
  20. * node harness/tools/verify-policy-card-fields.mjs
  21. */
  22. import { buildPolicyTableContent } from './_coordinator.mjs';
  23. let pass = 0;
  24. let fail = 0;
  25. const check = (name, cond, extra = '') => {
  26. if (cond) {
  27. pass++;
  28. console.log(' ok ' + name);
  29. } else {
  30. fail++;
  31. console.log(' FAIL ' + name + ' ' + extra);
  32. }
  33. };
  34. const POLICY_NAME = '关于进一步完善本区创业扶持政策有关事项的通知';
  35. /**
  36. * 按真实 `item` 事件的形状造数据(字段名与 probe 抓到的载荷一致)。
  37. * `cardName` 与 `title` 分开传:有申报事项后缀时两者不同,单事项时两者相同。
  38. */
  39. const makeItem = (title, sourceId, supportText, cardName = POLICY_NAME) => ({
  40. source_id: sourceId,
  41. title,
  42. knowledge_type: 'policy',
  43. is_policy_library: true,
  44. card: {
  45. kind: 'policy',
  46. name: { text: cardName, citations: [] },
  47. department: { text: '区人社局' },
  48. support_objects: { text: '对象' },
  49. support_methods_standards: { text: supportText },
  50. application_conditions: { text: '条件' },
  51. match_reason: { text: '原因' },
  52. },
  53. priority: { rank: 1 },
  54. });
  55. const items = [
  56. makeItem(`${POLICY_NAME} - 创业开办费补贴`, 'k_ff8fe476c2708c93b0d7803e', '一次性创业开办费补贴2万元,最高2万元补贴。'),
  57. makeItem(`${POLICY_NAME} - 青浦区级创业孵化基地的补贴和奖励`, 'k_6dc752bd7d30b42d430ca7fc', '分别给予40万/30万/20万运作经费补贴,最高40万元补贴'),
  58. ];
  59. const content = buildPolicyTableContent(items, []);
  60. const table = JSON.parse(content.replace(/^<!-- POLICY_TABLE /, '').replace(/ POLICY_TABLE -->$/, ''));
  61. const [a, b] = table.data;
  62. console.log('场景:同一政策下的两个申报事项\n');
  63. console.log('【1】详情面板的申报事项必须能区分');
  64. check('两条的 declaration_item 不同', a.declaration_item !== b.declaration_item, `${a.declaration_item} / ${b.declaration_item}`);
  65. check('第 1 条含「创业开办费补贴」', a.declaration_item === `${POLICY_NAME} - 创业开办费补贴`, a.declaration_item);
  66. check('第 2 条含「创业孵化基地」', b.declaration_item === `${POLICY_NAME} - 青浦区级创业孵化基地的补贴和奖励`, b.declaration_item);
  67. console.log('\n【2】卡片标题保持政策名不变(卡片区观感不动)');
  68. check('两条的 title 都是政策名', a.title === POLICY_NAME && b.title === POLICY_NAME, `${a.title}`);
  69. check('title 与 declaration_item 不再相同', a.title !== a.declaration_item);
  70. console.log('\n【3】没有后缀时退化为政策名,不出现空值');
  71. // 真实形态:单事项政策的 title 与 card.name.text 相同(见 probe 抓到的 item[1])
  72. const SOLO_NAME = '青浦区关于促进技能人才发展的实施办法';
  73. const single = buildPolicyTableContent([makeItem(SOLO_NAME, 'k_x', '补贴', SOLO_NAME)], []);
  74. const singleRow = JSON.parse(single.replace(/^<!-- POLICY_TABLE /, '').replace(/ POLICY_TABLE -->$/, '')).data[0];
  75. check('declaration_item 非空', !!singleRow.declaration_item, JSON.stringify(singleRow.declaration_item));
  76. check('无后缀时等于政策名(与 title 一致)', singleRow.declaration_item === SOLO_NAME && singleRow.title === SOLO_NAME, `${singleRow.declaration_item} / ${singleRow.title}`);
  77. console.log('\n【4】详情下标反查:字段可区分后能直接命中(与 resolvePolicyDetailIndex 配合)');
  78. // 模拟 policies 数组(normalizePolicy 之后),declaration_item 不同 → 唯一命中
  79. const policies = table.data.map((d) => ({ declaration_item: d.declaration_item, originalData: d }));
  80. const { resolvePolicyDetailIndex } = await import('./_policy-match.mjs');
  81. check(
  82. '用「同政策第 2 条」的字段反查 → 命中下标 1',
  83. resolvePolicyDetailIndex(policies, { originalData: { declaration_item: b.declaration_item } }) === 1,
  84. String(resolvePolicyDetailIndex(policies, { originalData: { declaration_item: b.declaration_item } }))
  85. );
  86. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  87. process.exit(fail ? 1 : 0);