| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- #!/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();
|