#!/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-next-actions-missing-proof-')); 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: 0, blocked_by_external_data: 1, fail: 0, pending: 0 }, rows: [] }); writeJson(path.join(outputs, 'proof-gap-closure-latest', 'proof-gap-closure-summary.json'), { complete: false, openCount: 1, rows: [ { id: 'video-ab-live-proof', title: '真实视频 A/B 验收', status: 'open', evidence: 'passed=true, mode=unknown, generatedBy=unknown, provider=ok', command: 'npm run acceptance:video-ab', expectedArtifact: '<视频A/B输出目录>/video-hit-rate-summary.json', required: 'acceptance:video-ab 必须在真实 provider 和真实视频资源下通过。', missingProofRequirements: [ 'proofContext.mode=live', 'proofContext.collectionMode=live', 'proofContext.generatedBy=acceptance:video-ab' ] } ] }); writeJson(path.join(outputs, 'optimization-pipeline-latest', 'optimization-pipeline-summary.json'), { generatedAt: '2026-06-07T08:59:39.964Z', readyForClaim: false, counts: { fail: 1 }, nextActions: [ { priority: 1, owner: '技术/AI', title: '过期 pipeline 视频 A/B 动作', step: 'video-ab-proof', command: 'stale pipeline command should not dispatch', expectedArtifact: 'stale/video-hit-rate-summary.json', acceptance: 'stale pipeline should be rerun first' } ] }); writeJson(path.join(outputs, 'optimization-completion-latest', 'optimization-completion-summary.json'), { complete: false, readyForClaim: false, proofGapOpenCount: 1, blockingReasons: [ { id: 'optimization-pipeline', evidence: 'readyForClaim=false, stale=true, stale optimization-pipeline-summary.json: generatedAt=2026-06-07T08:59:39.964Z older than latest proof source=2026-06-12T04:21:28.840Z', missingProofRequirements: [ 'optimizationPipeline.generatedAt>=currentProofSources', 'optimizationPipeline.readyForClaim=true' ], command: 'npm run optimization:pipeline -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict' } ] }); 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 = readJson(path.join(outputDir, 'business-proof-next-actions-summary.json')); const report = readText(path.join(outputDir, 'business-proof-next-actions-report.md')); const csv = readText(path.join(outputDir, 'business-proof-next-actions.csv')); const ownerReport = readText(path.join(outputDir, 'by-owner', 'tech-ai.md')); const videoAction = summary.actions.find(action => action.id === 'gap-video-ab-live-proof'); assert(summary.actionCount === 1, 'summary should expose top-level actionCount for stale-pipeline-only fixture'); assert(Array.isArray(summary.topActions) && summary.topActions.length === 1, 'summary should expose top-level topActions for current dispatch'); assert(summary.topActions[0].id === 'gap-video-ab-live-proof', 'topActions should keep the current video A/B action'); assert(summary.sourceState.pipelineStale === true, 'next actions should expose stale pipeline state from completion audit'); assert(summary.sourceState.pipelineNextActionCount === 0, 'stale pipeline next actions should not count as current dispatch'); assert(summary.sourceState.pipelineRawNextActionCount === 1, 'next actions should retain raw stale pipeline action count for audit'); assert(summary.sourceState.pipelineSuppressedNextActionCount === 1, 'next actions should expose suppressed stale pipeline action count'); assert(String(summary.sourceState.pipelineStaleReason || '').includes('stale=true'), 'next actions should expose stale pipeline reason'); assert(!summary.actions.some(action => action.command === 'stale pipeline command should not dispatch'), 'stale pipeline next action should not dispatch'); assert(videoAction, 'video A/B action should exist'); assert(Array.isArray(videoAction.missingProofRequirements), 'video A/B action should keep missingProofRequirements'); assert(videoAction.missingProofRequirements.includes('proofContext.mode=live'), 'video A/B action should keep missing mode requirement'); assert(videoAction.next.includes('proofContext.generatedBy=acceptance:video-ab'), 'video A/B next step should expose generatedBy requirement'); assert(report.includes('proofContext.collectionMode=live'), 'main report should expose missing collectionMode requirement'); assert(report.includes('pipeline.stale:true'), 'main report should expose stale pipeline state'); assert(report.includes('pipeline.suppressedNextActionCount:1'), 'main report should expose suppressed stale pipeline count'); assert(ownerReport.includes('proofContext.mode=live'), 'owner report should expose missing mode requirement'); assert(csv.includes('proofContext.generatedBy=acceptance:video-ab'), 'CSV should expose missing generatedBy requirement'); assert(!/sk-[A-Za-z0-9_-]{10,}/.test(report + csv + ownerReport), 'outputs should not contain model token patterns'); assert(!/r:[A-Za-z0-9]{20,}/.test(report + csv + ownerReport), 'outputs should not contain Parse sessionToken patterns'); console.log(JSON.stringify({ ok: true, outputDir, missingCount: videoAction.missingProofRequirements.length }, null, 2)); } function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value, null, 2), 'utf8'); } function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } function readText(file) { return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''); } function assert(condition, message) { if (!condition) throw new Error(message); } main();