business-proof-next-actions-smoke.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const root = path.resolve(__dirname, '..');
  7. function main() {
  8. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-business-proof-next-actions-'));
  9. const outputs = path.join(tmp, 'outputs');
  10. fs.mkdirSync(outputs, { recursive: true });
  11. writeJson(path.join(outputs, 'business-proof-progress-latest', 'business-proof-progress-summary.json'), {
  12. complete: false,
  13. counts: { pass: 4, blocked_by_external_data: 7, fail: 2, pending: 2 },
  14. rows: [
  15. { order: 3, owner: '商务', title: '填真实历史 Brief', status: 'blocked_by_external_data', evidence: 'historyReady=false, issues=4', next: '补真实历史 Brief。' },
  16. { order: 4, owner: '商务/投放', title: '填真实视频资源', status: 'blocked_by_external_data', evidence: 'realCandidate=0, failureCount=1', next: '补真实候选视频。' },
  17. { order: 5, owner: '技术/AI', title: '真实材料预审', status: 'fail', evidence: 'overallReady=false, failureCount=9', next: '重跑 intake。' },
  18. { order: 12, owner: '商务', title: '人工复核标注', status: 'blocked_by_external_data', evidence: 'reviewMetricsReady=false, customerEffectReady=false', next: '补人工复核。' },
  19. { order: 14, owner: '技术/AI', title: '客户效果审计', status: 'blocked_by_external_data', evidence: 'missing customer-effect-summary.json', next: '补客户选择。' }
  20. ]
  21. });
  22. writeJson(path.join(outputs, 'proof-gap-closure-latest', 'proof-gap-closure-summary.json'), {
  23. complete: false,
  24. openCount: 4,
  25. rows: [
  26. {
  27. id: 'historical-dataset',
  28. title: '真实历史 Brief 数据集',
  29. status: 'open',
  30. evidence: 'missing historical-dataset-audit.json',
  31. command: 'npm run history:audit',
  32. expectedArtifact: 'historical-dataset-audit.json',
  33. required: 'readyForCustomerEffectProof=true',
  34. missingProofRequirements: ['missing historical-dataset-audit.json']
  35. },
  36. {
  37. id: 'video-real-candidate',
  38. title: '真实候选视频资源',
  39. status: 'open',
  40. evidence: 'realCandidate=0',
  41. command: 'npm run video:resource-readiness',
  42. expectedArtifact: 'video-resource-readiness-summary.json',
  43. required: 'readyForVideoAbPreflight=true',
  44. missingProofRequirements: ['acceptance.readyForVideoAbPreflight=true', 'counts.realCandidateRows>=1']
  45. },
  46. {
  47. id: 'manual-review-and-customer-effect',
  48. title: '商务复核和客户效果证明',
  49. status: 'open',
  50. evidence: 'missing customer-effect-summary.json',
  51. command: 'npm run customer-effect:audit',
  52. expectedArtifact: 'customer-effect-summary.json',
  53. required: 'customer-effect:audit overallPass=true',
  54. missingProofRequirements: ['missing customer-effect-summary.json']
  55. }
  56. ]
  57. });
  58. writeJson(path.join(outputs, 'intake-readiness-latest', 'intake-readiness-summary.json'), {
  59. failureCount: 3,
  60. history: { issues: [{ message: '真实历史 Brief 数不足:1/5。' }] },
  61. repairActions: [
  62. {
  63. priority: 1,
  64. section: 'history',
  65. type: 'min-briefs',
  66. owner: '商务',
  67. file: 'outputs/data-intake-pack-latest/history-data-template.csv',
  68. field: 'brief编号',
  69. action: '补齐至少 5 个真实历史 Brief。',
  70. acceptance: 'historyReady=true。'
  71. },
  72. {
  73. priority: 2,
  74. section: 'review',
  75. type: 'customer-decision',
  76. owner: '商务',
  77. file: 'outputs/data-intake-pack-latest/manual-review-template.csv',
  78. field: '客户选择',
  79. action: '补齐客户最终选中/拒绝记录。',
  80. acceptance: 'customerEffectReady=true。'
  81. }
  82. ],
  83. issues: [
  84. { section: 'history', type: 'min-briefs', message: '真实历史 Brief 数不足:1/5。' },
  85. { section: 'video', type: 'real-candidate', message: '缺少真实候选视频资源。' },
  86. { section: 'review', type: 'customer-decision', message: '人工复核表至少需要客户最终选中/拒绝记录。' }
  87. ]
  88. });
  89. writeJson(path.join(outputs, 'video-resource-readiness-latest', 'video-resource-readiness-summary.json'), {
  90. failureCount: 1,
  91. repairActions: [
  92. {
  93. priority: 1,
  94. owner: '商务/投放',
  95. type: 'missing-real-candidate',
  96. file: 'outputs/video-intake-pack-latest/video-resource-template.csv',
  97. field: '资源角色/是否真实资源',
  98. action: '补真实候选视频。',
  99. acceptance: 'hasRealCandidateResource=true。'
  100. }
  101. ],
  102. issues: [{ message: '缺少标记为真实资源的候选视频。' }]
  103. });
  104. writeText(path.join(outputs, 'intake-readiness-latest', 'intake-readiness-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,商务,brief编号,补齐至少 5 个真实历史 Brief\n');
  105. writeText(path.join(outputs, 'video-resource-readiness-latest', 'video-resource-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,商务/投放,资源角色/是否真实资源,补真实候选视频\n');
  106. writeJson(path.join(outputs, 'homepage-evidence-readiness-latest', 'homepage-evidence-readiness-summary.json'), {
  107. ready: false,
  108. directCustomerProof: false,
  109. proofLevel: 'not_business_proof',
  110. failureCount: 2,
  111. repairActions: [
  112. {
  113. priority: 1,
  114. owner: '技术/AI',
  115. type: 'missing-provider-evidence',
  116. file: 'outputs/software-client-latest/software-client-list.final.csv',
  117. field: 'homepageEvidence.source',
  118. action: '接入真实主页近期内容 provider 或提供 path/local JSON 证据。',
  119. acceptance: '至少 1 个 creator 的 evidenceSource 为 provider/path/api。'
  120. },
  121. {
  122. priority: 2,
  123. owner: '商务/投放',
  124. type: 'missing-recent-posts',
  125. file: 'outputs/software-client-latest/software-client-list.final.csv',
  126. field: 'recentPosts',
  127. action: '补最近 10/20 篇内容列表。',
  128. acceptance: 'recentPosts 条数大于 0。'
  129. }
  130. ]
  131. });
  132. writeText(path.join(outputs, 'homepage-evidence-readiness-latest', 'homepage-evidence-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,技术/AI,homepageEvidence.source,接入真实主页近期内容 provider\n2,商务/投放,recentPosts,补最近 10/20 篇内容列表\n');
  133. writeJson(path.join(outputs, 'optimization-pipeline-latest', 'optimization-pipeline-summary.json'), {
  134. readyForClaim: false,
  135. counts: { pass: 5, fail: 3 },
  136. nextActions: [
  137. {
  138. priority: 3,
  139. owner: '商务',
  140. title: '补齐历史数据审计字段',
  141. step: 'history-audit',
  142. command: '补齐真实 Brief、参考链接、人工名单、客户选择、拒绝原因和历史人工补号量基线后重跑 history:audit --strict。',
  143. expectedArtifact: 'history-audit/historical-dataset-audit.json',
  144. acceptance: 'historical-dataset-audit readyForCustomerEffectProof=true。'
  145. },
  146. {
  147. priority: 4,
  148. owner: '商务',
  149. title: '补齐客户效果证明',
  150. step: 'customer-effect-audit',
  151. command: '补齐客户最终选择、历史人工补号量基线和本轮人工补号量后重跑 customer-effect:audit --strict。',
  152. expectedArtifact: 'customer-effect-summary.json',
  153. acceptance: 'customer-effect:audit overallPass=true。'
  154. },
  155. {
  156. priority: 4,
  157. owner: '商务/投放',
  158. title: '补齐真实视频资源',
  159. step: 'video-resource-readiness',
  160. command: '补齐真实参考视频、真实候选视频、视频 URL、封面、ASR、帧图或正文证据后重跑 video:resource-readiness --strict。',
  161. expectedArtifact: 'video-resource-readiness-summary.json',
  162. acceptance: 'video-resource-readiness readyForVideoAbPreflight=true。'
  163. }
  164. ]
  165. });
  166. const outputDir = path.join(outputs, 'business-proof-next-actions-latest');
  167. const result = spawnSync(process.execPath, [
  168. path.join(root, 'scripts', 'business-proof-next-actions.js'),
  169. '--outputs',
  170. outputs,
  171. '--output',
  172. outputDir
  173. ], { cwd: root, encoding: 'utf8' });
  174. if (result.status !== 0) {
  175. process.stderr.write(result.stderr || result.stdout || '');
  176. throw new Error('business-proof-next-actions should run');
  177. }
  178. const summary = JSON.parse(fs.readFileSync(path.join(outputDir, 'business-proof-next-actions-summary.json'), 'utf8'));
  179. const report = fs.readFileSync(path.join(outputDir, 'business-proof-next-actions-report.md'), 'utf8').replace(/^\uFEFF/, '');
  180. const csv = fs.readFileSync(path.join(outputDir, 'business-proof-next-actions.csv'), 'utf8').replace(/^\uFEFF/, '');
  181. assert(summary.complete === false, 'next actions should not mark complete');
  182. assert(summary.actionCount === summary.actions.length, 'summary should expose top-level actionCount matching actions');
  183. assert(summary.ownerGroupCount === summary.ownerGroups.length, 'summary should expose top-level ownerGroupCount');
  184. assert(summary.ownerRepairArtifactCount === summary.ownerRepairArtifacts.length, 'summary should expose top-level ownerRepairArtifactCount');
  185. assert(Array.isArray(summary.topActions) && summary.topActions.length === Math.min(5, summary.actions.length), 'summary should expose top-level topActions');
  186. assert(summary.topActions[0].id === summary.actions[0].id, 'topActions should follow the sorted action queue');
  187. assert(summary.guardrails.some(item => item.includes('round:refresh')), 'next action guardrails should prefer ordered round refresh');
  188. assert(!summary.guardrails.some(item => item.includes('intake:readiness、proof-gap:closure、business-proof:progress 和 round:deposition')), 'next action guardrails should not preserve stale manual refresh order');
  189. assert(summary.actions.length >= 3, 'next actions should include actionable blockers after compacting duplicate validation steps');
  190. assert(Array.isArray(summary.ownerGroups) && summary.ownerGroups.length >= 2, 'next actions should group actions by owner');
  191. assert(Array.isArray(summary.ownerArtifacts) && summary.ownerArtifacts.length === summary.ownerGroups.length, 'next actions should write owner artifacts');
  192. assert(summary.ownerArtifactCount === summary.ownerArtifacts.length, 'summary should expose top-level ownerArtifactCount after owner artifacts are written');
  193. assert(Array.isArray(summary.ownerRepairArtifacts) && summary.ownerRepairArtifacts.length >= 2, 'next actions should expose owner repair artifacts');
  194. assert(summary.ownerRepairArtifacts.some(item => item.owner === '商务' && item.csv.includes('intake-readiness-repair-actions.csv') && item.actionCount === 2), 'business owner should receive data intake repair artifact');
  195. assert(summary.ownerRepairArtifacts.some(item => item.owner === '商务/投放' && item.csv.includes('video-resource-repair-actions.csv') && item.actionCount === 1), 'business/media owner should receive video repair artifact');
  196. assert(summary.ownerGroups.some(group => group.owner === '商务' && group.actionCount >= 1), 'owner groups should include business owner');
  197. assert(summary.ownerArtifacts.every(item => fs.existsSync(path.join(outputDir, item.markdown)) && fs.existsSync(path.join(outputDir, item.csv))), 'owner artifact files should exist');
  198. assert(summary.ownerArtifacts.some(item => item.owner === '商务' && item.repairArtifacts.some(artifact => artifact.csv.includes('intake-readiness-repair-actions.csv'))), 'business owner artifact metadata should include data repair csv');
  199. assert(summary.ownerArtifacts.some(item => item.owner === '商务/投放' && item.repairArtifacts.some(artifact => artifact.csv.includes('video-resource-repair-actions.csv'))), 'business/media owner artifact metadata should include video repair csv');
  200. assert(summary.actions[0].title.includes('填真实历史 Brief') || summary.actions[0].title.includes('history'), 'history blocker should be first');
  201. assert(summary.actions.some(action => action.title.includes('填真实视频资源')), 'video resource action should be present');
  202. assert(!summary.actions.some(action => action.title.includes('修复 intake history')), 'history intake issues should be merged into the history action');
  203. assert(!summary.actions.some(action => action.title.includes('修复 intake video')), 'video intake issues should be merged into the video action');
  204. assert(!summary.actions.some(action => action.title.includes('修复 intake review')), 'review intake issues should be merged into the review action');
  205. assert(!summary.actions.some(action => action.title.includes('真实材料预审')), 'intake readiness validation should merge into a primary business gap action');
  206. assert(!summary.actions.some(action => action.title.includes('历史数据审计')), 'history audit validation should merge into the primary history action');
  207. assert(!summary.actions.some(action => action.title.includes('视频资源就绪审计')), 'video readiness validation should merge into the primary video action');
  208. assert(!summary.actions.some(action => action.title.includes('商务复核指标')), 'review metrics validation should merge into the primary review action');
  209. assert(!summary.actions.some(action => ['生成统一收集包', '生成视频资源收集包', '刷新证据和交接'].some(title => action.title.includes(title))), 'setup and deposition steps should not appear as business proof actions');
  210. assert(!summary.actions.some(action => action.source === 'proof-gap-closure' && action.title.includes('真实历史 Brief')), 'historical proof-gap action should not duplicate progress actions');
  211. assert(summary.actions.some(action => action.title.includes('填真实历史 Brief') && action.next.includes('真实历史 Brief 数不足')), 'history action should retain issue details');
  212. assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.next.includes('缺少真实候选视频资源')), 'video action should retain issue details');
  213. assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.next.includes('客户最终选中/拒绝记录')), 'review action should retain issue details');
  214. assert(summary.actions.some(action => action.title.includes('填真实历史 Brief') && action.missingProofRequirements.includes('missing historical-dataset-audit.json')), 'history action should inherit proof-gap missing requirements');
  215. assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.missingProofRequirements.includes('counts.realCandidateRows>=1')), 'video action should inherit proof-gap missing requirements');
  216. assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.missingProofRequirements.includes('missing customer-effect-summary.json')), 'review action should inherit proof-gap missing requirements');
  217. assert(summary.sourceState.pipelineNextActionCount === 3, 'summary should include pipeline next action count');
  218. assert(!summary.actions.some(action => action.title.includes('补齐历史数据审计字段')), 'pipeline history action should merge into the primary history action when intake is not ready');
  219. assert(!summary.actions.some(action => action.title === '补齐真实视频资源'), 'pipeline video action should merge into the primary video action when video intake is not ready');
  220. assert(!summary.actions.some(action => action.title.includes('补齐客户效果证明')), 'pipeline customer-effect action should merge into the primary review action when review intake is not ready');
  221. assert(summary.actions.some(action => action.title.includes('填真实历史 Brief') && action.source.includes('optimization-pipeline') && action.next.includes('pipeline 当前动作')), 'history action should retain pipeline action details');
  222. assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.source.includes('optimization-pipeline') && action.next.includes('pipeline 当前动作')), 'video action should retain pipeline action details');
  223. assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.source.includes('optimization-pipeline') && action.next.includes('pipeline 当前动作')), 'review action should retain pipeline customer-effect details');
  224. assert(summary.actions.some(action => action.title.includes('填真实历史 Brief') && action.command === '填写 outputs\\data-intake-pack-latest\\history-data-template.csv'), 'history primary action should keep the fill-template command');
  225. assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.command === '填写 outputs\\video-intake-pack-latest\\video-resource-template.csv'), 'video primary action should keep the fill-template command');
  226. assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.command === '填写 outputs\\data-intake-pack-latest\\manual-review-template.csv'), 'review primary action should keep the fill-template command');
  227. assert(summary.ownerGroups.some(group => group.owner === '商务' && group.topTitles.some(title => title.includes('填真实历史 Brief'))), 'owner groups should keep primary business action visible');
  228. assert(summary.ownerArtifacts.some(item => item.owner === '商务/投放' && item.markdown === 'by-owner/business-media.md'), 'business/media owner should use the normalized artifact name');
  229. assert(!summary.ownerArtifacts.some(item => item.markdown === 'by-owner/media-business.md'), 'media/business duplicate owner artifact should not be generated');
  230. assert(summary.actions.some(action => [action.next, action.command, action.acceptance].join(' ').includes('intake')), 'intake readiness validation should be retained in merged action details');
  231. assert(report.includes('# 提号补证下一步行动队列'), 'report should be Chinese');
  232. assert(report.includes('## 按负责人汇总'), 'report should include owner summary section');
  233. assert(report.includes('## 负责人文件'), 'report should include owner artifact section');
  234. assert(report.includes('修复清单附件'), 'report should include owner repair artifact links');
  235. assert(report.includes('intake-readiness-repair-actions.csv'), 'report should include data intake repair csv');
  236. assert(report.includes('video-resource-repair-actions.csv'), 'report should include video repair csv');
  237. assert(report.includes('避免把“填一张表”拆成多条重复任务'), 'report should explain duplicate action merge rule');
  238. assert(report.includes('下一步说明'), 'report should expose next-step details');
  239. assert(report.includes('pipeline.nextActionCount'), 'report should expose pipeline next action count');
  240. assert(report.includes('pipeline 当前动作'), 'report should include pipeline merged action details');
  241. assert(report.includes('真实历史 Brief 数不足'), 'report should show merged history issue details');
  242. assert(report.includes('missing historical-dataset-audit.json'), 'report should expose inherited historical proof requirement');
  243. assert(report.includes('counts.realCandidateRows>=1'), 'report should expose inherited video proof requirement');
  244. assert(report.includes('missing customer-effect-summary.json'), 'report should expose inherited customer-effect proof requirement');
  245. assert(report.includes('不能把 sample、smoke'), 'report should preserve proof boundary');
  246. assert(report.includes('round:refresh'), 'report should expose ordered round refresh after real materials are filled');
  247. assert(csv.includes('优先级,负责人,动作'), 'csv should have Chinese headers');
  248. const businessOwnerReport = fs.readFileSync(path.join(outputDir, 'by-owner', 'business.md'), 'utf8').replace(/^\uFEFF/, '');
  249. const mediaOwnerReport = fs.readFileSync(path.join(outputDir, 'by-owner', 'business-media.md'), 'utf8').replace(/^\uFEFF/, '');
  250. assert(businessOwnerReport.includes('修复清单附件') && businessOwnerReport.includes('intake-readiness-repair-actions.csv'), 'business owner report should link data repair csv');
  251. assert(mediaOwnerReport.includes('修复清单附件') && mediaOwnerReport.includes('video-resource-repair-actions.csv'), 'business/media owner report should link video repair csv');
  252. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns');
  253. assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns');
  254. console.log(JSON.stringify({ ok: true, outputDir, actionCount: summary.actions.length, topAction: summary.actions[0].title }, null, 2));
  255. }
  256. function writeJson(file, value) {
  257. fs.mkdirSync(path.dirname(file), { recursive: true });
  258. fs.writeFileSync(file, JSON.stringify(value, null, 2), 'utf8');
  259. }
  260. function writeText(file, value) {
  261. fs.mkdirSync(path.dirname(file), { recursive: true });
  262. fs.writeFileSync(file, value, 'utf8');
  263. }
  264. function assert(condition, message) {
  265. if (!condition) throw new Error(message);
  266. }
  267. main();