#!/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-real-proof-intake-bundle-')); const result = spawnSync(process.execPath, [ 'scripts/real-proof-intake-bundle.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, 'real proof intake bundle should pass against current outputs'); const summaryPath = path.join(outputDir, 'real-proof-intake-bundle-summary.json'); const reportPath = path.join(outputDir, 'real-proof-intake-bundle.md'); const csvPath = path.join(outputDir, 'real-proof-intake-bundle.csv'); assert(fs.existsSync(summaryPath), 'summary should exist'); assert(fs.existsSync(reportPath), 'report should exist'); assert(fs.existsSync(csvPath), '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 expectedOwnerArtifactCount = Number(summary.expectedOperatorPackOwnerArtifactCount || summary.operatorPackOwnerArtifactCount || 0); assert(summary.passed === true, 'summary should pass'); assert(summary.complete === false, 'bundle should keep incomplete state while proof gaps remain'); assert(summary.directCustomerProof === false, 'bundle must not be direct customer proof'); assert(summary.proofLevel === 'not_business_proof', 'bundle should classify as not business proof'); assert(summary.proofOpenCount >= 1, 'bundle should expose open proof gap count'); assert(summary.fillRowCount >= 3, 'bundle should expose history/review/video fill rows'); assert(summary.proofGapRowCount === 5, 'bundle should expose five proof gap rows'); assert(summary.recheckCommandCount >= 5, 'bundle should expose recheck commands'); assert(summary.exportRowCount >= summary.fillRowCount + summary.proofGapRowCount, 'csv export should include fill and proof rows'); assert(summary.operatorPackEvidenceRowCount === 5, 'bundle should expose operator pack evidence row count'); assert(summary.operatorPackNextActionCount === 5, 'bundle should expose operator pack next action count'); assert(summary.operatorPackMissingProofRequirementCount >= 1, 'bundle should expose missing proof requirement count'); assert(summary.operatorPackOwnerArtifactCount === expectedOwnerArtifactCount, 'bundle should expose operator pack owner artifact count'); assert(Array.isArray(summary.operatorPackOwnerArtifacts) && summary.operatorPackOwnerArtifacts.length === expectedOwnerArtifactCount, 'bundle should expose operator pack owner artifacts'); assert(summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务' && item.markdown?.includes('by-owner/business.md') && item.csv?.includes('by-owner/business.csv')), 'bundle should expose business owner operator files'); assert(summary.operatorPackOwnerArtifacts.some(item => item.owner === '技术/AI' && item.markdown?.includes('by-owner/tech-ai.md') && item.csv?.includes('by-owner/tech-ai.csv')), 'bundle should expose tech/AI owner operator files'); if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) { assert(summary.operatorPackOwnerArtifacts.some(item => item.markdown?.includes('by-owner/business-media.md') && item.csv?.includes('by-owner/business-media.csv')), 'bundle should expose business/media owner operator files when that owner has open actions'); } assert(summary.operatorPackOwnerArtifacts.every(item => Number(item.evidenceRowCount || 0) >= 1 && Number(item.nextActionCount || 0) >= 1), 'bundle owner artifacts should preserve evidence and next action counts'); assert(summary.proofGapRows.every(item => item.evidence), 'every proof gap row should carry current evidence'); assert(summary.proofGapRows.every(item => item.nextAction), 'every proof gap row should carry next action'); assert(summary.proofGapRows.some(item => Array.isArray(item.missingProofRequirements) && item.missingProofRequirements.length >= 1), 'bundle should carry missing proof requirements from operator pack'); assert(summary.exportRows.filter(item => item.type === '证明缺口').every(item => item.evidence && item.nextAction), 'proof gap export rows should carry evidence and next action'); const videoAbProofGap = summary.proofGapRows.find(item => item.gapId === 'video-ab-live-proof'); assert(videoAbProofGap, 'bundle should include video A/B live proof gap'); assertVideoAbPreflightContext(videoAbProofGap, 'bundle video A/B proof gap row'); const videoAbExportRow = summary.exportRows.find(item => item.type === '证明缺口' && item.evidence?.includes('preflight.readyForVideoAb')); assert(videoAbExportRow, 'bundle csv export rows should include video A/B preflight evidence row'); assertTextIncludesAll(videoAbExportRow.evidence, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle export row should preserve video A/B preflight evidence context'); assertTextIncludesAll(videoAbExportRow.nextAction, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle export row should preserve video A/B preflight missing requirements'); assert(summary.templateFiles.some(item => item.path?.includes('history-data-template.csv')), 'history template should be included'); assert(summary.templateFiles.some(item => item.path?.includes('manual-review-template.csv')), 'manual review template should be included'); assert(summary.templateFiles.some(item => item.path?.includes('video-resource-template.csv')), 'video template should be included'); assert(summary.templateFiles.some(item => item.path?.includes('intake-field-checklist.md')), 'field checklist should be included'); assert(summary.templateFiles.some(item => item.path?.includes('proof-gap-operator-pack.md')), 'operator pack should be included'); assert(summary.templateFiles.some(item => item.path?.includes('optimization-completion-report.md')), 'optimization completion report should be included'); assert(summary.templateFiles.some(item => item.status?.includes('readyForClaim=false')), 'optimization completion template status should expose readyForClaim=false'); assert(summary.templateFiles.some(item => item.path?.includes('long-run-readiness-report.md')), 'long-run readiness report should be included'); assert(summary.templateFiles.some(item => item.status?.includes('ready=false')), 'long-run readiness template status should expose ready=false'); assert(summary.fillRows.some(item => item.section === 'history' && item.owner === '商务'), 'history fill row should belong to business'); assert(summary.fillRows.some(item => item.section === 'video' && item.owner === '商务/投放'), 'video fill row should belong to business/media'); assert(summary.proofGapRows.some(item => item.gapId === 'manual-review-and-customer-effect'), 'customer effect proof gap should be included'); assert(summary.repairRows.some(item => item.csv?.includes('intake-readiness-repair-actions.csv')), 'data repair csv should be included'); assert(summary.repairRows.some(item => item.csv?.includes('video-resource-repair-actions.csv')), 'video repair csv should be included'); assert(summary.recheckCommands.some(item => item.command.includes('intake:readiness')), 'intake readiness recheck should be included'); assert(summary.recheckCommands.some(item => item.command.includes('video:resource-readiness')), 'video readiness recheck should be included'); assert(summary.recheckCommands.some(item => item.command.includes('customer-effect:audit')), 'customer effect audit should be included'); assert(summary.recheckCommands.some(item => item.command.includes('acceptance:video-ab')), 'video A/B acceptance should be included'); assert(summary.recheckCommands.some(item => item.command.includes('longrun:readiness')), 'long-run readiness recheck should be included'); assert(summary.recheckCommands.some(item => item.command.includes('optimization:completion')), 'optimization completion recheck should be included'); assert(summary.recheckCommands.some(item => item.command.includes('round:refresh')), 'round refresh should be included'); assert(summary.guardrails.some(item => item.includes('proofOpenCount')), 'guardrails should mention proofOpenCount'); assert(summary.guardrails.some(item => item.includes('readyForClaim=false')), 'guardrails should mention readyForClaim=false'); assert(summary.guardrails.some(item => item.includes('longrun:readiness.ready=false')), 'guardrails should mention longrun readiness false'); assert(summary.sourceState.operatorPackEvidenceRowCount === 5, 'source state should expose operator pack evidence row count'); assert(summary.sourceState.operatorPackNextActionCount === 5, 'source state should expose operator pack next action count'); assert(summary.sourceState.operatorPackMissingProofRequirementCount >= 1, 'source state should expose missing proof requirement count'); assert(summary.sourceState.operatorPackOwnerArtifactCount === expectedOwnerArtifactCount, 'source state should expose operator pack owner artifact count'); assert(summary.sourceState.optimizationCompletionComplete === false, 'source state should expose optimization completion incomplete'); assert(summary.sourceState.optimizationCompletionReadyForClaim === false, 'source state should expose optimization completion not ready for claim'); assert(summary.sourceState.longRunReadinessReady === false, 'source state should expose long-run readiness not ready'); assert(summary.sourceState.longRunReadinessFailCount >= 1, 'source state should expose long-run readiness fail count'); assert(summary.sourceState.longRunReadinessProofLevel === 'not_business_proof', 'source state should classify long-run readiness as not business proof'); assert(summary.sourceState.longRunReadinessDirectCustomerProof === false, 'source state should not treat long-run readiness as customer proof'); assert(report.includes('# 真实验收材料收集总包'), 'report should be Chinese'); assert(report.includes('history-data-template.csv'), 'report should include history template path'); assert(report.includes('manual-review-template.csv'), 'report should include manual review template path'); assert(report.includes('video-resource-template.csv'), 'report should include video template path'); assert(report.includes('proof-gap-operator-pack.md'), 'report should include operator pack path'); assert(report.includes('intake-field-checklist.md'), 'report should include field checklist path'); assert(report.includes('optimization-completion-report.md'), 'report should include optimization completion path'); assert(report.includes('readyForClaim=false'), 'report should include optimization completion boundary'); assert(report.includes('long-run-readiness-report.md'), 'report should include long-run readiness path'); assert(report.includes('ready=false'), 'report should include long-run readiness boundary'); assert(report.includes('## 补齐后复验命令'), 'report should include recheck command section'); assert(report.includes('operatorPackEvidenceRowCount:5'), 'report should include operator pack evidence row count'); assert(report.includes('operatorPackNextActionCount:5'), 'report should include operator pack next action count'); assert(report.includes(`operatorPackOwnerArtifactCount:${expectedOwnerArtifactCount}`), 'report should include operator pack owner artifact count'); assert(report.includes('## 操作包负责人附件'), 'report should include operator owner artifact section'); assert(report.includes('by-owner/business.md'), 'report should include business owner operator markdown'); if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) { assert(report.includes('by-owner/business-media.csv'), 'report should include business/media owner operator csv when present'); } assert(report.includes('by-owner/tech-ai.md'), 'report should include tech/AI owner operator markdown'); assert(report.includes('当前证据'), 'report should include current evidence column'); assert(report.includes('下一步'), 'report should include next action column'); assert(report.includes('briefCount=1'), 'report should include concrete history audit evidence'); assert(report.includes('briefCount>=5'), 'report should include concrete history missing proof requirement'); assert(report.includes('acceptance.customerSelectedRateMeasured=true'), 'report should include concrete customer-effect missing proof requirement'); assert(report.includes('补 5-10 个真实 Brief'), 'report should include next action text'); assert(report.includes('npm run longrun:readiness'), 'report should include long-run readiness recheck command'); assert(report.includes('npm run optimization:completion'), 'report should include optimization completion recheck command'); assert(report.includes('不证明客户效果'), 'report should keep proof boundary'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle report should preserve video A/B preflight evidence context'); assertTextIncludesAll(report, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle report should preserve video A/B preflight missing requirements'); assert(csv.includes('类型,顺序,负责人,标题,文件或命令'), 'csv should include Chinese header'); assert(csv.includes('当前证据'), 'csv should include current evidence column'); assert(csv.includes('下一步'), 'csv should include next action column'); assert(csv.includes('填表顺序'), 'csv should include fill rows'); assert(csv.includes('证明缺口'), 'csv should include proof gap rows'); assert(csv.includes('负责人附件'), 'csv should include owner artifact rows'); assert(csv.includes('by-owner/business.csv'), 'csv should include business owner operator csv'); if (summary.operatorPackOwnerArtifacts.some(item => item.owner === '商务/投放')) { assert(csv.includes('by-owner/business-media.md'), 'csv should include business/media owner operator markdown when present'); } assert(csv.includes('by-owner/tech-ai.csv'), 'csv should include tech/AI owner operator csv'); assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_EVIDENCE_TOKENS, 'bundle csv should preserve video A/B preflight evidence context'); assertTextIncludesAll(csv, VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, 'bundle csv should preserve video A/B preflight missing requirements'); 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, proofOpenCount: summary.proofOpenCount, exportRowCount: summary.exportRowCount }, 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.recheckCommands.some(command => command.includes('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`); assertTextIncludesAll(row.nextAction || '', VIDEO_AB_PREFLIGHT_REQUIREMENT_TOKENS, `${label} next action 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();