#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const root = path.resolve(__dirname, '..'); function main() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-business-proof-next-actions-')); const outputs = path.join(tmp, 'outputs'); fs.mkdirSync(outputs, { recursive: true }); writeJson(path.join(outputs, 'business-proof-progress-latest', 'business-proof-progress-summary.json'), { complete: false, counts: { pass: 4, blocked_by_external_data: 7, fail: 2, pending: 2 }, rows: [ { order: 3, owner: '商务', title: '填真实历史 Brief', status: 'blocked_by_external_data', evidence: 'historyReady=false, issues=4', next: '补真实历史 Brief。' }, { order: 4, owner: '商务/投放', title: '填真实视频资源', status: 'blocked_by_external_data', evidence: 'realCandidate=0, failureCount=1', next: '补真实候选视频。' }, { order: 5, owner: '技术/AI', title: '真实材料预审', status: 'fail', evidence: 'overallReady=false, failureCount=9', next: '重跑 intake。' }, { order: 12, owner: '商务', title: '人工复核标注', status: 'blocked_by_external_data', evidence: 'reviewMetricsReady=false, customerEffectReady=false', next: '补人工复核。' }, { order: 14, owner: '技术/AI', title: '客户效果审计', status: 'blocked_by_external_data', evidence: 'missing customer-effect-summary.json', next: '补客户选择。' } ] }); writeJson(path.join(outputs, 'proof-gap-closure-latest', 'proof-gap-closure-summary.json'), { complete: false, openCount: 4, rows: [ { id: 'historical-dataset', title: '真实历史 Brief 数据集', status: 'open', evidence: 'missing historical-dataset-audit.json', command: 'npm run history:audit', expectedArtifact: 'historical-dataset-audit.json', required: 'readyForCustomerEffectProof=true', missingProofRequirements: ['missing historical-dataset-audit.json'] }, { id: 'video-real-candidate', title: '真实候选视频资源', status: 'open', evidence: 'realCandidate=0', command: 'npm run video:resource-readiness', expectedArtifact: 'video-resource-readiness-summary.json', required: 'readyForVideoAbPreflight=true', missingProofRequirements: ['acceptance.readyForVideoAbPreflight=true', 'counts.realCandidateRows>=1'] }, { id: 'manual-review-and-customer-effect', title: '商务复核和客户效果证明', status: 'open', evidence: 'missing customer-effect-summary.json', command: 'npm run customer-effect:audit', expectedArtifact: 'customer-effect-summary.json', required: 'customer-effect:audit overallPass=true', missingProofRequirements: ['missing customer-effect-summary.json'] } ] }); writeJson(path.join(outputs, 'intake-readiness-latest', 'intake-readiness-summary.json'), { failureCount: 3, history: { issues: [{ message: '真实历史 Brief 数不足:1/5。' }] }, repairActions: [ { priority: 1, section: 'history', type: 'min-briefs', owner: '商务', file: 'outputs/data-intake-pack-latest/history-data-template.csv', field: 'brief编号', action: '补齐至少 5 个真实历史 Brief。', acceptance: 'historyReady=true。' }, { priority: 2, section: 'review', type: 'customer-decision', owner: '商务', file: 'outputs/data-intake-pack-latest/manual-review-template.csv', field: '客户选择', action: '补齐客户最终选中/拒绝记录。', acceptance: 'customerEffectReady=true。' } ], issues: [ { section: 'history', type: 'min-briefs', message: '真实历史 Brief 数不足:1/5。' }, { section: 'video', type: 'real-candidate', message: '缺少真实候选视频资源。' }, { section: 'review', type: 'customer-decision', message: '人工复核表至少需要客户最终选中/拒绝记录。' } ] }); writeJson(path.join(outputs, 'video-resource-readiness-latest', 'video-resource-readiness-summary.json'), { failureCount: 1, repairActions: [ { priority: 1, owner: '商务/投放', type: 'missing-real-candidate', file: 'outputs/video-intake-pack-latest/video-resource-template.csv', field: '资源角色/是否真实资源', action: '补真实候选视频。', acceptance: 'hasRealCandidateResource=true。' } ], issues: [{ message: '缺少标记为真实资源的候选视频。' }] }); writeText(path.join(outputs, 'intake-readiness-latest', 'intake-readiness-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,商务,brief编号,补齐至少 5 个真实历史 Brief\n'); writeText(path.join(outputs, 'video-resource-readiness-latest', 'video-resource-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,商务/投放,资源角色/是否真实资源,补真实候选视频\n'); writeJson(path.join(outputs, 'homepage-evidence-readiness-latest', 'homepage-evidence-readiness-summary.json'), { ready: false, directCustomerProof: false, proofLevel: 'not_business_proof', failureCount: 2, repairActions: [ { priority: 1, owner: '技术/AI', type: 'missing-provider-evidence', file: 'outputs/software-client-latest/software-client-list.final.csv', field: 'homepageEvidence.source', action: '接入真实主页近期内容 provider 或提供 path/local JSON 证据。', acceptance: '至少 1 个 creator 的 evidenceSource 为 provider/path/api。' }, { priority: 2, owner: '商务/投放', type: 'missing-recent-posts', file: 'outputs/software-client-latest/software-client-list.final.csv', field: 'recentPosts', action: '补最近 10/20 篇内容列表。', acceptance: 'recentPosts 条数大于 0。' } ] }); writeText(path.join(outputs, 'homepage-evidence-readiness-latest', 'homepage-evidence-repair-actions.csv'), '优先级,负责人,字段,修复动作\n1,技术/AI,homepageEvidence.source,接入真实主页近期内容 provider\n2,商务/投放,recentPosts,补最近 10/20 篇内容列表\n'); writeJson(path.join(outputs, 'optimization-pipeline-latest', 'optimization-pipeline-summary.json'), { readyForClaim: false, counts: { pass: 5, fail: 3 }, nextActions: [ { priority: 3, owner: '商务', title: '补齐历史数据审计字段', step: 'history-audit', command: '补齐真实 Brief、参考链接、人工名单、客户选择、拒绝原因和历史人工补号量基线后重跑 history:audit --strict。', expectedArtifact: 'history-audit/historical-dataset-audit.json', acceptance: 'historical-dataset-audit readyForCustomerEffectProof=true。' }, { priority: 4, owner: '商务', title: '补齐客户效果证明', step: 'customer-effect-audit', command: '补齐客户最终选择、历史人工补号量基线和本轮人工补号量后重跑 customer-effect:audit --strict。', expectedArtifact: 'customer-effect-summary.json', acceptance: 'customer-effect:audit overallPass=true。' }, { priority: 4, owner: '商务/投放', title: '补齐真实视频资源', step: 'video-resource-readiness', command: '补齐真实参考视频、真实候选视频、视频 URL、封面、ASR、帧图或正文证据后重跑 video:resource-readiness --strict。', expectedArtifact: 'video-resource-readiness-summary.json', acceptance: 'video-resource-readiness readyForVideoAbPreflight=true。' } ] }); const outputDir = path.join(outputs, 'business-proof-next-actions-latest'); const result = spawnSync(process.execPath, [ path.join(root, 'scripts', 'business-proof-next-actions.js'), '--outputs', outputs, '--output', outputDir ], { cwd: root, encoding: 'utf8' }); if (result.status !== 0) { process.stderr.write(result.stderr || result.stdout || ''); throw new Error('business-proof-next-actions should run'); } const summary = JSON.parse(fs.readFileSync(path.join(outputDir, 'business-proof-next-actions-summary.json'), 'utf8')); const report = fs.readFileSync(path.join(outputDir, 'business-proof-next-actions-report.md'), 'utf8').replace(/^\uFEFF/, ''); const csv = fs.readFileSync(path.join(outputDir, 'business-proof-next-actions.csv'), 'utf8').replace(/^\uFEFF/, ''); assert(summary.complete === false, 'next actions should not mark complete'); assert(summary.actionCount === summary.actions.length, 'summary should expose top-level actionCount matching actions'); assert(summary.ownerGroupCount === summary.ownerGroups.length, 'summary should expose top-level ownerGroupCount'); assert(summary.ownerRepairArtifactCount === summary.ownerRepairArtifacts.length, 'summary should expose top-level ownerRepairArtifactCount'); assert(Array.isArray(summary.topActions) && summary.topActions.length === Math.min(5, summary.actions.length), 'summary should expose top-level topActions'); assert(summary.topActions[0].id === summary.actions[0].id, 'topActions should follow the sorted action queue'); assert(summary.guardrails.some(item => item.includes('round:refresh')), 'next action guardrails should prefer ordered round refresh'); 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'); assert(summary.actions.length >= 3, 'next actions should include actionable blockers after compacting duplicate validation steps'); assert(Array.isArray(summary.ownerGroups) && summary.ownerGroups.length >= 2, 'next actions should group actions by owner'); assert(Array.isArray(summary.ownerArtifacts) && summary.ownerArtifacts.length === summary.ownerGroups.length, 'next actions should write owner artifacts'); assert(summary.ownerArtifactCount === summary.ownerArtifacts.length, 'summary should expose top-level ownerArtifactCount after owner artifacts are written'); assert(Array.isArray(summary.ownerRepairArtifacts) && summary.ownerRepairArtifacts.length >= 2, 'next actions should expose owner repair artifacts'); 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'); 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'); assert(summary.ownerGroups.some(group => group.owner === '商务' && group.actionCount >= 1), 'owner groups should include business owner'); assert(summary.ownerArtifacts.every(item => fs.existsSync(path.join(outputDir, item.markdown)) && fs.existsSync(path.join(outputDir, item.csv))), 'owner artifact files should exist'); 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'); 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'); assert(summary.actions[0].title.includes('填真实历史 Brief') || summary.actions[0].title.includes('history'), 'history blocker should be first'); assert(summary.actions.some(action => action.title.includes('填真实视频资源')), 'video resource action should be present'); assert(!summary.actions.some(action => action.title.includes('修复 intake history')), 'history intake issues should be merged into the history action'); assert(!summary.actions.some(action => action.title.includes('修复 intake video')), 'video intake issues should be merged into the video action'); assert(!summary.actions.some(action => action.title.includes('修复 intake review')), 'review intake issues should be merged into the review action'); assert(!summary.actions.some(action => action.title.includes('真实材料预审')), 'intake readiness validation should merge into a primary business gap action'); assert(!summary.actions.some(action => action.title.includes('历史数据审计')), 'history audit validation should merge into the primary history action'); assert(!summary.actions.some(action => action.title.includes('视频资源就绪审计')), 'video readiness validation should merge into the primary video action'); assert(!summary.actions.some(action => action.title.includes('商务复核指标')), 'review metrics validation should merge into the primary review action'); assert(!summary.actions.some(action => ['生成统一收集包', '生成视频资源收集包', '刷新证据和交接'].some(title => action.title.includes(title))), 'setup and deposition steps should not appear as business proof actions'); assert(!summary.actions.some(action => action.source === 'proof-gap-closure' && action.title.includes('真实历史 Brief')), 'historical proof-gap action should not duplicate progress actions'); assert(summary.actions.some(action => action.title.includes('填真实历史 Brief') && action.next.includes('真实历史 Brief 数不足')), 'history action should retain issue details'); assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.next.includes('缺少真实候选视频资源')), 'video action should retain issue details'); assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.next.includes('客户最终选中/拒绝记录')), 'review action should retain issue details'); 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'); assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.missingProofRequirements.includes('counts.realCandidateRows>=1')), 'video action should inherit proof-gap missing requirements'); assert(summary.actions.some(action => action.title.includes('人工复核标注') && action.missingProofRequirements.includes('missing customer-effect-summary.json')), 'review action should inherit proof-gap missing requirements'); assert(summary.sourceState.pipelineNextActionCount === 3, 'summary should include pipeline next action count'); assert(!summary.actions.some(action => action.title.includes('补齐历史数据审计字段')), 'pipeline history action should merge into the primary history action when intake is not ready'); assert(!summary.actions.some(action => action.title === '补齐真实视频资源'), 'pipeline video action should merge into the primary video action when video intake is not ready'); 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'); 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'); assert(summary.actions.some(action => action.title.includes('填真实视频资源') && action.source.includes('optimization-pipeline') && action.next.includes('pipeline 当前动作')), 'video action should retain pipeline action details'); 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'); 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'); 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'); 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'); assert(summary.ownerGroups.some(group => group.owner === '商务' && group.topTitles.some(title => title.includes('填真实历史 Brief'))), 'owner groups should keep primary business action visible'); assert(summary.ownerArtifacts.some(item => item.owner === '商务/投放' && item.markdown === 'by-owner/business-media.md'), 'business/media owner should use the normalized artifact name'); assert(!summary.ownerArtifacts.some(item => item.markdown === 'by-owner/media-business.md'), 'media/business duplicate owner artifact should not be generated'); assert(summary.actions.some(action => [action.next, action.command, action.acceptance].join(' ').includes('intake')), 'intake readiness validation should be retained in merged action details'); assert(report.includes('# 提号补证下一步行动队列'), 'report should be Chinese'); assert(report.includes('## 按负责人汇总'), 'report should include owner summary section'); assert(report.includes('## 负责人文件'), 'report should include owner artifact section'); assert(report.includes('修复清单附件'), 'report should include owner repair artifact links'); assert(report.includes('intake-readiness-repair-actions.csv'), 'report should include data intake repair csv'); assert(report.includes('video-resource-repair-actions.csv'), 'report should include video repair csv'); assert(report.includes('避免把“填一张表”拆成多条重复任务'), 'report should explain duplicate action merge rule'); assert(report.includes('下一步说明'), 'report should expose next-step details'); assert(report.includes('pipeline.nextActionCount'), 'report should expose pipeline next action count'); assert(report.includes('pipeline 当前动作'), 'report should include pipeline merged action details'); assert(report.includes('真实历史 Brief 数不足'), 'report should show merged history issue details'); assert(report.includes('missing historical-dataset-audit.json'), 'report should expose inherited historical proof requirement'); assert(report.includes('counts.realCandidateRows>=1'), 'report should expose inherited video proof requirement'); assert(report.includes('missing customer-effect-summary.json'), 'report should expose inherited customer-effect proof requirement'); assert(report.includes('不能把 sample、smoke'), 'report should preserve proof boundary'); assert(report.includes('round:refresh'), 'report should expose ordered round refresh after real materials are filled'); assert(csv.includes('优先级,负责人,动作'), 'csv should have Chinese headers'); const businessOwnerReport = fs.readFileSync(path.join(outputDir, 'by-owner', 'business.md'), 'utf8').replace(/^\uFEFF/, ''); const mediaOwnerReport = fs.readFileSync(path.join(outputDir, 'by-owner', 'business-media.md'), 'utf8').replace(/^\uFEFF/, ''); assert(businessOwnerReport.includes('修复清单附件') && businessOwnerReport.includes('intake-readiness-repair-actions.csv'), 'business owner report should link data repair csv'); assert(mediaOwnerReport.includes('修复清单附件') && mediaOwnerReport.includes('video-resource-repair-actions.csv'), 'business/media owner report should link video repair csv'); assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report), 'report should not contain model token patterns'); assert(!/r:[A-Za-z0-9]{20,}/.test(report), 'report should not contain Parse sessionToken patterns'); console.log(JSON.stringify({ ok: true, outputDir, actionCount: summary.actions.length, topAction: summary.actions[0].title }, null, 2)); } function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value, null, 2), 'utf8'); } function writeText(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, value, 'utf8'); } function assert(condition, message) { if (!condition) throw new Error(message); } main();