export-proof-gap-software-form.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const DEFAULT_INPUT = path.join(ROOT, 'docs', 'ai-optimization-proof-gap-software-table.md');
  6. const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'tihao-proof-gap-software-form-latest');
  7. const SOFTWARE_HEADER = [
  8. 'brief编号',
  9. '策略',
  10. '排名',
  11. '平台',
  12. '博主名称',
  13. '综合分',
  14. 'brief匹配分',
  15. '参考风格分',
  16. '主页证据分',
  17. '视觉质感分',
  18. '调性一致分',
  19. '证据加分',
  20. '证据风险扣分',
  21. '推荐理由',
  22. '风险提示',
  23. '主页链接',
  24. '人工复核标签'
  25. ];
  26. function main() {
  27. const args = parseArgs(process.argv.slice(2));
  28. const input = path.resolve(args.input || DEFAULT_INPUT);
  29. const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
  30. const result = buildProofGapSoftwareForm({ input, outputDir });
  31. fs.mkdirSync(outputDir, { recursive: true });
  32. fs.writeFileSync(result.files.csv, withBom(renderCsv([SOFTWARE_HEADER, ...result.rows])), 'utf8');
  33. fs.writeFileSync(result.files.markdown, withBom(renderMarkdown(result)), 'utf8');
  34. fs.writeFileSync(result.files.summary, JSON.stringify(result.summary, null, 2), 'utf8');
  35. console.log(JSON.stringify({
  36. outputDir,
  37. csv: result.files.csv,
  38. markdown: result.files.markdown,
  39. summary: result.files.summary,
  40. rowCount: result.summary.rowCount,
  41. duplicateIdCount: result.summary.duplicateIdCount,
  42. headerMatches: result.summary.headerMatches
  43. }, null, 2));
  44. if (args.strict && !result.summary.passed) process.exitCode = 1;
  45. }
  46. function buildProofGapSoftwareForm({ input, outputDir }) {
  47. const text = fs.readFileSync(input, 'utf8').replace(/^\uFEFF/, '');
  48. const rows = text.split(/\r?\n/).map(splitMarkdownRow).filter(cells => cells.length);
  49. const header = rows.find(cells => cells[0] === 'brief编号') || [];
  50. const dataRows = rows.filter(cells => /^GAP-\d{3}$/.test(cells[0] || ''));
  51. const ids = dataRows.map(cells => cells[0]);
  52. const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
  53. const headerMatches = JSON.stringify(header) === JSON.stringify(SOFTWARE_HEADER);
  54. const expectedTitles = [
  55. '真实历史 Brief 数据集',
  56. '真实视频资源表',
  57. '真实视频证据 A/B',
  58. '真实 live/provider 长跑证明',
  59. '商务复核和客户效果'
  60. ];
  61. const actualTitles = dataRows.map(cells => cells[4]);
  62. const missingTitles = expectedTitles.filter(title => !actualTitles.includes(title));
  63. const rowWidthOk = dataRows.every(cells => cells.length === SOFTWARE_HEADER.length);
  64. const summary = {
  65. generatedAt: new Date().toISOString(),
  66. input: path.relative(ROOT, input).replace(/\\/g, '/'),
  67. outputDir: path.relative(ROOT, outputDir).replace(/\\/g, '/'),
  68. headerMatches,
  69. rowWidthOk,
  70. rowCount: dataRows.length,
  71. duplicateIdCount: duplicateIds.length,
  72. duplicateIds: Array.from(new Set(duplicateIds)),
  73. expectedTitles,
  74. actualTitles,
  75. missingTitles,
  76. passed: headerMatches && rowWidthOk && dataRows.length === 5 && duplicateIds.length === 0 && missingTitles.length === 0,
  77. guardrails: [
  78. '本表单用于安排补证任务,不证明客户效果完成。',
  79. '不能把 sample、smoke、模板、provider fallback 或接口 200 当成业务证明。',
  80. '表单不得包含 sessionToken、Authorization、模型 token 或 npm token。'
  81. ]
  82. };
  83. return {
  84. rows: dataRows,
  85. summary,
  86. files: {
  87. csv: path.join(outputDir, 'tihao-proof-gap-software-form.csv'),
  88. markdown: path.join(outputDir, 'tihao-proof-gap-software-form.md'),
  89. summary: path.join(outputDir, 'tihao-proof-gap-software-form-summary.json')
  90. }
  91. };
  92. }
  93. function renderMarkdown(result) {
  94. const lines = [
  95. '# 最新提号补证表单(软件端格式)',
  96. '',
  97. `- 生成时间:${result.summary.generatedAt}`,
  98. `- 行数:${result.summary.rowCount}`,
  99. `- 表头匹配:${result.summary.headerMatches ? '是' : '否'}`,
  100. `- 是否通过:${result.summary.passed ? '是' : '否'}`,
  101. '',
  102. '| ' + SOFTWARE_HEADER.join(' | ') + ' |',
  103. '| ' + SOFTWARE_HEADER.map(() => '---').join(' | ') + ' |',
  104. ...result.rows.map(row => '| ' + row.map(escapeMarkdownCell).join(' | ') + ' |'),
  105. '',
  106. '## 边界',
  107. '',
  108. ...result.summary.guardrails.map(item => `- ${item}`)
  109. ];
  110. return lines.join('\n');
  111. }
  112. function renderCsv(rows) {
  113. return rows.map(row => row.map(csvCell).join(',')).join('\n');
  114. }
  115. function splitMarkdownRow(line) {
  116. if (!line || !line.trim().startsWith('|')) return [];
  117. return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim());
  118. }
  119. function parseArgs(argv) {
  120. const args = {};
  121. for (let index = 0; index < argv.length; index += 1) {
  122. const raw = argv[index];
  123. if (!raw.startsWith('--')) continue;
  124. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  125. const next = argv[index + 1];
  126. if (!next || next.startsWith('--')) args[key] = true;
  127. else {
  128. args[key] = next;
  129. index += 1;
  130. }
  131. }
  132. return args;
  133. }
  134. function csvCell(value) {
  135. const text = String(value ?? '');
  136. return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  137. }
  138. function escapeMarkdownCell(value) {
  139. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  140. }
  141. function withBom(text) {
  142. return `\uFEFF${text}`;
  143. }
  144. if (require.main === module) main();
  145. module.exports = {
  146. SOFTWARE_HEADER,
  147. buildProofGapSoftwareForm
  148. };