#!/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, '..'); const VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS = [ 'mode=unknown', 'collectionMode=unknown', 'generatedBy=unknown', 'preflight.readyForVideoAb=false', 'preflight.failureCount=4', 'preflight.generatedBy=acceptance:video-ab-preflight', 'preflight.proofLevel=not_business_proof', 'preflight.canCloseProofGap=false', 'runtimeCredentialPresent=false', 'companyResolved=false', 'vocSocialProviderTokenPresent=false', 'videoAnalysisTokenPresent=false' ]; const VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS = [ 'proofContext.mode=live', 'proofContext.collectionMode=live', 'proofContext.generatedBy=acceptance:video-ab', 'proofContext.requiresRuntimeCredential=true', 'proofContext.requiresVocSocialProvider=true', 'proofContext.requiresVideoAnalysisProvider=true', 'videoAbPreflight.readyForVideoAb=true', 'videoAbPreflight.proofContext.requiresRuntimeCredential=true', 'videoAbPreflight.company resolved for live run', 'videoAbPreflight.proofContext.requiresVocSocialProvider=true', 'videoAbPreflight.proofContext.requiresVideoAnalysisProvider=true' ]; function main() { const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-proof-gap-operator-pack-')); const result = spawnSync(process.execPath, [ 'scripts/proof-gap-operator-pack.js', '--output', outputDir, '--strict' ], { cwd: ROOT, encoding: 'utf8', shell: false }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); assert(result.status === 0, 'proof gap operator pack should pass against current outputs'); const summaryPath = path.join(outputDir, 'proof-gap-operator-pack-summary.json'); const reportPath = path.join(outputDir, 'proof-gap-operator-pack.md'); const csvPath = path.join(outputDir, 'proof-gap-operator-pack.csv'); const businessOwnerPath = path.join(outputDir, 'by-owner', 'business.md'); const businessOwnerCsvPath = path.join(outputDir, 'by-owner', 'business.csv'); const mediaOwnerPath = path.join(outputDir, 'by-owner', 'business-media.md'); const techOwnerPath = path.join(outputDir, 'by-owner', 'tech-ai.md'); const techOwnerCsvPath = path.join(outputDir, 'by-owner', 'tech-ai.csv'); assert(fs.existsSync(summaryPath), 'summary should exist'); assert(fs.existsSync(reportPath), 'report should exist'); assert(fs.existsSync(csvPath), 'csv should exist'); assert(fs.existsSync(businessOwnerPath), 'business owner markdown should exist'); assert(fs.existsSync(businessOwnerCsvPath), 'business owner csv should exist'); assert(fs.existsSync(techOwnerPath), 'tech/AI owner markdown should exist'); assert(fs.existsSync(techOwnerCsvPath), 'tech/AI owner csv should exist'); const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); const report = fs.readFileSync(reportPath, 'utf8').replace(/^\uFEFF/, ''); const csv = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, ''); const businessOwnerReport = fs.readFileSync(businessOwnerPath, 'utf8').replace(/^\uFEFF/, ''); const businessOwnerCsv = fs.readFileSync(businessOwnerCsvPath, 'utf8').replace(/^\uFEFF/, ''); const techOwnerReport = fs.readFileSync(techOwnerPath, 'utf8').replace(/^\uFEFF/, ''); const techOwnerCsv = fs.readFileSync(techOwnerCsvPath, 'utf8').replace(/^\uFEFF/, ''); const hasBusinessMediaOwnerArtifact = Array.isArray(summary.ownerArtifacts) && summary.ownerArtifacts.some(item => item.markdown === 'by-owner/business-media.md'); if (hasBusinessMediaOwnerArtifact) { assert(fs.existsSync(mediaOwnerPath), 'business/media owner markdown should exist when generated'); } assert(summary.passed === true, 'summary should pass'); assert(summary.complete === false, 'summary should keep incomplete state while proof gaps remain'); assert(summary.directCustomerProof === false, 'operator pack must not be direct customer proof'); assert(summary.proofLevel === 'not_business_proof', 'operator pack should classify as not business proof'); assert(summary.rowCount === 5, 'operator pack should expose five business proof gaps'); assert(summary.openCount >= 1, 'operator pack should expose open proof gaps'); assert(Array.isArray(summary.gapIds) && summary.gapIds.includes('historical-dataset') && summary.gapIds.includes('manual-review-and-customer-effect'), 'operator pack should expose stable gap ids'); assert(summary.fillFileCount >= 3, 'operator pack should expose unique fill file count'); assert(summary.recheckCommandCount >= 5, 'operator pack should expose unique recheck command count'); assert(summary.ownerGroupCount >= 2, 'operator pack should expose owner group count'); assert(summary.ownerArtifactCount === summary.ownerGroups.length, 'operator pack should expose one owner artifact per current open owner group'); assert(Array.isArray(summary.ownerArtifacts) && summary.ownerArtifacts.length === summary.ownerGroups.length, 'operator pack should expose owner artifacts'); assert(summary.ownerArtifacts.some(item => item.owner === '商务' && item.markdown === 'by-owner/business.md' && item.csv === 'by-owner/business.csv'), 'operator pack should expose business owner artifact'); if (summary.ownerGroups.some(item => item.owner === '商务/投放')) { assert(summary.ownerArtifacts.some(item => item.owner === '商务/投放' && item.markdown === 'by-owner/business-media.md' && item.csv === 'by-owner/business-media.csv'), 'operator pack should expose business/media owner artifact when that owner has open actions'); } assert(summary.ownerArtifacts.some(item => item.owner === '技术/AI' && item.markdown === 'by-owner/tech-ai.md' && item.csv === 'by-owner/tech-ai.csv'), 'operator pack should expose tech/AI owner artifact'); assert(summary.ownerArtifacts.every(item => item.evidenceRowCount >= 1 && item.nextActionCount >= 1), 'every owner artifact should expose evidence and next action counts'); assert(summary.evidenceRowCount === 5, 'operator pack should expose current evidence for every gap row'); assert(summary.nextActionCount === 5, 'operator pack should expose next action for every gap row'); assert(summary.missingProofRequirementCount >= 1, 'operator pack should expose missing proof requirement count'); assert(summary.rows.some(row => row.gapId === 'historical-dataset' && row.owner === '商务'), 'history gap should be assigned to business'); assert(summary.rows.some(row => row.gapId === 'video-real-candidate' && row.owner === '商务/投放'), 'video resource gap should be assigned to business/media'); assert(summary.rows.some(row => row.gapId === 'video-ab-live-proof' && row.owner === '技术/AI'), 'video A/B gap should be assigned to tech/AI'); const videoAbRow = summary.rows.find(row => row.gapId === 'video-ab-live-proof'); assert(videoAbRow, 'video A/B gap row should exist'); assertVideoAbPreflightContext(videoAbRow, 'operator pack video A/B row'); assert(summary.rows.some(row => row.gapId === 'manual-review-and-customer-effect' && row.fillFiles.some(file => file.includes('manual-review-template.csv'))), 'customer effect gap should point to manual review template'); assert(summary.rows.every(row => row.fillFiles.length >= 1), 'every row should expose fill files'); assert(summary.rows.every(row => row.recheckCommands.length >= 1), 'every row should expose recheck commands'); assert(summary.rows.every(row => row.primaryFillFile && row.primaryRecheckCommand), 'every row should expose primary fill/recheck fields'); assert(summary.rows.every(row => row.evidence && row.nextAction), 'every row should expose evidence and next action'); assert(summary.rows.every(row => /不.*证明|不得宣称|不等同于/.test(row.boundary || '')), 'every row should keep proof boundary'); assert(summary.ownerGroups.length >= 2, 'operator pack should expose owner groups'); assert(summary.guardrails.some(item => item.includes('proofOpenCount')), 'guardrails should mention proofOpenCount'); assert(report.includes('# 提号补证操作包'), 'report should be Chinese'); assert(report.includes('缺口 ID'), 'report should include gap id summary'); assert(report.includes('复验命令数'), 'report should include recheck command count'); assert(report.includes('## 负责人文件'), 'report should include owner artifact section'); assert(report.includes('by-owner/business.md'), 'report should include business owner markdown'); if (hasBusinessMediaOwnerArtifact) { assert(report.includes('by-owner/business-media.csv'), 'report should include business/media owner csv when generated'); } assert(report.includes('by-owner/tech-ai.md'), 'report should include tech/AI owner markdown'); assert(report.includes('当前证据行数'), 'report should include evidence row count'); assert(report.includes('下一步动作数'), 'report should include next action count'); assert(report.includes('当前证据'), 'report should include current evidence column'); assert(report.includes('下一步'), 'report should include next action column'); assert(report.includes('history-data-template.csv'), 'report should include history template path'); assert(report.includes('video-resource-template.csv'), 'report should include video template path'); assert(report.includes('customer-effect:audit'), 'report should include customer effect audit command'); assert(csv.includes('缺口ID'), 'csv should include Chinese header'); assert(csv.includes('主填写文件'), 'csv should include primary fill file header'); assert(csv.includes('首个复验命令'), 'csv should include primary recheck command header'); assert(csv.includes('当前证据'), 'csv should include current evidence header'); assert(csv.includes('下一步'), 'csv should include next action header'); assert(businessOwnerReport.includes('# 商务提号补证操作包'), 'business owner report should have owner title'); assert(businessOwnerReport.includes('missing historical-dataset-audit.json'), 'business owner report should include current evidence'); assert(businessOwnerReport.includes('补 5-10 个真实 Brief'), 'business owner report should include next action'); assert(businessOwnerCsv.includes('当前证据'), 'business owner csv should include current evidence header'); assert(businessOwnerCsv.includes('下一步'), 'business owner csv should include next action header'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'operator pack report should preserve video A/B preflight evidence context'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'operator pack report should preserve video A/B preflight missing requirements'); assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'operator pack csv should preserve video A/B preflight evidence context'); assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI owner report should preserve video A/B preflight evidence context'); assertTextIncludesAll(techOwnerReport, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'tech/AI owner report should preserve video A/B preflight missing requirements'); assertTextIncludesAll(techOwnerCsv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'tech/AI owner csv should preserve video A/B preflight evidence context'); 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'); assert(!/Authorization/i.test(report), 'report should not expose Authorization text'); console.log(JSON.stringify({ ok: true, outputDir, rowCount: summary.rowCount, openCount: summary.openCount }, null, 2)); } function assert(condition, message) { if (!condition) throw new Error(message); } function assertVideoAbPreflightContext(row, label) { assert(row.status === 'open', `${label} should remain open until live proof exists`); assert(row.directCustomerProof !== true, `${label} should not be direct customer proof`); assert(Array.isArray(row.recheckCommands) && row.recheckCommands.includes('npm run acceptance:video-ab'), `${label} should keep live video A/B acceptance command`); assertTextIncludesAll(row.evidence || '', VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, `${label} should preserve non-proof preflight evidence context`); assertTextIncludesAll(row.missingProofRequirements || [], VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} should preserve live proof requirements`); } function assertTextIncludesAll(value, tokens, message) { const text = Array.isArray(value) ? value.join('\n') : String(value || ''); const missing = tokens.filter(token => !text.includes(token)); assert(missing.length === 0, `${message}: missing ${missing.join(', ')}`); } if (require.main === module) main();