#!/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-proof-gap-')); const handoff = path.join(tmp, 'optimization-handoff-summary.json'); const evidence = path.join(tmp, 'evidence-index-summary.json'); const videoReadiness = path.join(tmp, 'video-resource-readiness-summary.json'); const closure = path.join(tmp, 'proof-gap-closure-summary.json'); const output = path.join(tmp, 'proof-gap-output'); fs.writeFileSync(handoff, JSON.stringify({ complete: false, statusCounts: { passed: 22, ready_not_proven: 2, blocked_by_external_data: 2 }, evidenceCounts: { real_evidence: 5, smoke_or_local: 22, not_business_proof: 85 }, readyNotProven: [{ id: 'video-ab-proof' }, { id: 'historical-dataset' }], externalBlockers: [{ id: 'live-provider-overnight-proof' }, { id: 'customer-effect-proof' }] }, null, 2), 'utf8'); fs.writeFileSync(evidence, JSON.stringify({ counts: { real_evidence: 5, smoke_or_local: 22, not_business_proof: 85 } }, null, 2), 'utf8'); fs.writeFileSync(videoReadiness, JSON.stringify({ acceptance: { readyForVideoAbPreflight: false }, failureCount: 1, counts: { realReferenceRows: 1, realCandidateRows: 0, videoUrlRows: 1 } }, null, 2), 'utf8'); fs.writeFileSync(closure, JSON.stringify({ complete: false, openCount: 5, closedCount: 0 }, null, 2), 'utf8'); const result = spawnSync(process.execPath, [ path.join(root, 'scripts', 'generate-proof-gap-request.js'), '--handoff', handoff, '--evidence', evidence, '--video-readiness', videoReadiness, '--closure', closure, '--output', output ], { cwd: root, encoding: 'utf8' }); if (result.status !== 0) { process.stderr.write(result.stderr || result.stdout || ''); throw new Error('generate-proof-gap-request should pass'); } const summary = JSON.parse(fs.readFileSync(path.join(output, 'proof-gap-request-summary.json'), 'utf8')); const report = fs.readFileSync(path.join(output, 'proof-gap-request-report.md'), 'utf8').replace(/^\uFEFF/, ''); const csv = fs.readFileSync(path.join(output, 'proof-gap-request-table.csv'), 'utf8').replace(/^\uFEFF/, ''); assert(summary.complete === false, 'proof gap request must not mark long-term goal complete'); assert(summary.unresolvedCount >= 4, 'proof gap request should list unresolved proof gaps'); assert(summary.rows.some(row => row.id === 'video-real-candidate' && row.status === 'missing_real_data'), 'should request real candidate video resources'); assert(summary.rows.some(row => row.id === 'manual-review-and-customer-effect'), 'should request customer effect proof data'); assert(summary.rows.every(row => row.templateCommand && row.templateArtifact), 'every proof gap row should include template command and artifact'); assert(summary.rows.some(row => row.id === 'historical-dataset' && row.templateCommand.includes('data:intake-template')), 'historical dataset gap should point to data intake template'); assert(summary.rows.some(row => row.id === 'video-real-candidate' && row.templateCommand.includes('video:intake-template')), 'video gap should point to video intake template'); assert(summary.observed.proofGapClosureOpenCount === 5, 'proof gap request should expose latest closure open count'); assert(summary.rows.every(row => row.owner !== '技术/投放' && row.owner !== '商务/客户' && row.owner !== '投放/商务'), 'owner labels should be normalized'); assert(report.includes('# 提号真实证明缺口请求包'), 'report should be Chinese'); assert(report.includes('模板命令') && report.includes('模板产物'), 'report should expose intake template columns'); assert(report.includes('closure.openCount:5'), 'report should include closure open count'); assert(csv.startsWith('缺口ID,负责人,状态'), 'CSV should have business-facing Chinese header'); assert(csv.includes('模板命令') && csv.includes('模板产物'), 'CSV should expose intake template columns'); assert(!/(sk-[A-Za-z0-9_-]{20,}|r:[A-Za-z0-9]{20,}|Authorization\s*[:=]\s*Bearer)/i.test(report + csv), 'proof gap request should not contain secrets'); fs.writeFileSync(videoReadiness, JSON.stringify({ acceptance: { readyForVideoAbPreflight: true }, failureCount: 0, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 } }, null, 2), 'utf8'); fs.writeFileSync(closure, JSON.stringify({ complete: false, openCount: 3, closedCount: 2, rows: [ { id: 'video-real-candidate', status: 'closed', evidence: 'realReference=1, realCandidate=1' }, { id: 'live-provider-overnight-proof', status: 'closed', evidence: 'liveEnabled=true, overallPass=true' }, { id: 'historical-dataset', status: 'open' }, { id: 'video-ab-live-proof', status: 'open' }, { id: 'manual-review-and-customer-effect', status: 'open' } ] }, null, 2), 'utf8'); const closureAwareOutput = path.join(tmp, 'proof-gap-output-closure-aware'); const closureAware = spawnSync(process.execPath, [ path.join(root, 'scripts', 'generate-proof-gap-request.js'), '--handoff', handoff, '--evidence', evidence, '--video-readiness', videoReadiness, '--closure', closure, '--output', closureAwareOutput ], { cwd: root, encoding: 'utf8' }); if (closureAware.status !== 0) { process.stderr.write(closureAware.stderr || closureAware.stdout || ''); throw new Error('closure-aware generate-proof-gap-request should pass'); } const closureAwareSummary = JSON.parse(fs.readFileSync(path.join(closureAwareOutput, 'proof-gap-request-summary.json'), 'utf8')); assert(closureAwareSummary.unresolvedCount === 3, `closure-aware request should follow closure openCount=3, got ${closureAwareSummary.unresolvedCount}`); assert(closureAwareSummary.rows.some(row => row.id === 'live-provider-overnight-proof' && row.status === 'ready'), 'closed live-provider gap should stay ready in request package'); assert(closureAwareSummary.rows.some(row => row.id === 'video-real-candidate' && row.status === 'ready'), 'closed video resource gap should stay ready in request package'); console.log(JSON.stringify({ ok: true, output, gapCount: summary.rows.length, unresolvedCount: summary.unresolvedCount }, null, 2)); } function assert(condition, message) { if (!condition) throw new Error(message); } main();