#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const OUTPUTS = path.join(ROOT, 'outputs'); const DEFAULT_OUTPUT = path.join(OUTPUTS, 'real-proof-intake-bundle-latest'); function main() { const args = parseArgs(process.argv.slice(2)); const outputsDir = path.resolve(args.outputs || OUTPUTS); const outputDir = path.resolve(args.output || DEFAULT_OUTPUT); const summary = buildRealProofIntakeBundle({ root: ROOT, outputsDir }); fs.mkdirSync(outputDir, { recursive: true }); 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'); fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8'); fs.writeFileSync(csvPath, withBom(renderCsv(summary.exportRows)), 'utf8'); console.log(JSON.stringify({ outputDir, summary: summaryPath, report: reportPath, csv: csvPath, passed: summary.passed, complete: summary.complete, proofOpenCount: summary.proofOpenCount, exportRowCount: summary.exportRowCount }, null, 2)); if (args.strict && !summary.passed) process.exitCode = 1; } function buildRealProofIntakeBundle({ root, outputsDir }) { const dirs = { dataPack: pickDir(outputsDir, /^data-intake-pack-/) || path.join(outputsDir, 'data-intake-pack-latest'), videoPack: pickDir(outputsDir, /^video-intake-pack-/) || path.join(outputsDir, 'video-intake-pack-latest'), intakeChecklist: pickDir(outputsDir, /^intake-field-checklist-/) || path.join(outputsDir, 'intake-field-checklist-latest'), operatorPack: pickDir(outputsDir, /^proof-gap-operator-pack-/) || path.join(outputsDir, 'proof-gap-operator-pack-latest'), proofGapClosure: pickDir(outputsDir, /^proof-gap-closure-/) || path.join(outputsDir, 'proof-gap-closure-latest'), latestFormIndex: pickDir(outputsDir, /^latest-form-index-/) || path.join(outputsDir, 'latest-form-index-latest'), manualReviewGuide: pickDir(outputsDir, /^manual-review-label-guide-/) || path.join(outputsDir, 'manual-review-label-guide-latest'), businessExecution: pickDir(outputsDir, /^business-execution-index-/) || path.join(outputsDir, 'business-execution-index-latest'), intakeReadiness: pickDir(outputsDir, /^intake-readiness-/) || path.join(outputsDir, 'intake-readiness-latest'), videoReadiness: pickDir(outputsDir, /^video-resource-readiness-/) || path.join(outputsDir, 'video-resource-readiness-latest'), homepageReadiness: pickDir(outputsDir, /^homepage-evidence-readiness-/) || path.join(outputsDir, 'homepage-evidence-readiness-latest'), planStatus: pickDir(outputsDir, /^plan-execution-status-/) || path.join(outputsDir, 'plan-execution-status-latest'), optimizationCompletion: pickDir(outputsDir, /^optimization-completion-/) || path.join(outputsDir, 'optimization-completion-latest'), longRunReadiness: pickDir(outputsDir, /^long-run-readiness-/) || path.join(outputsDir, 'long-run-readiness-latest') }; const summaries = { intakeChecklist: readJson(path.join(dirs.intakeChecklist, 'intake-field-checklist-summary.json')), operatorPack: readJson(path.join(dirs.operatorPack, 'proof-gap-operator-pack-summary.json')), proofGapClosure: readJson(path.join(dirs.proofGapClosure, 'proof-gap-closure-summary.json')), latestFormIndex: readJson(path.join(dirs.latestFormIndex, 'latest-form-index-summary.json')), manualReviewGuide: readJson(path.join(dirs.manualReviewGuide, 'manual-review-label-guide-summary.json')), businessExecution: readJson(path.join(dirs.businessExecution, 'business-execution-index-summary.json')), intakeReadiness: readJson(path.join(dirs.intakeReadiness, 'intake-readiness-summary.json')), videoReadiness: readJson(path.join(dirs.videoReadiness, 'video-resource-readiness-summary.json')), homepageReadiness: readJson(path.join(dirs.homepageReadiness, 'homepage-evidence-readiness-summary.json')), planStatus: readJson(path.join(dirs.planStatus, 'plan-execution-status-summary.json')), optimizationCompletion: readJson(path.join(dirs.optimizationCompletion, 'optimization-completion-summary.json')), longRunReadiness: readJson(path.join(dirs.longRunReadiness, 'long-run-readiness-summary.json')) }; const templateFiles = buildTemplateFiles(root, dirs, summaries); const fillRows = buildFillRows(summaries); const proofGapRows = buildProofGapRows(summaries.operatorPack, summaries.proofGapClosure); const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts(root, dirs.operatorPack, summaries.operatorPack); const repairRows = buildRepairRows(root, dirs, summaries); const recheckCommands = buildRecheckCommands(summaries); const exportRows = [ ...templateFiles.map((item, index) => exportRow('打开文件', index + 1, item.owner, item.title, item.path, item.status, item.usage, item.boundary)), ...operatorPackOwnerArtifacts.map((item, index) => exportRow( '负责人附件', index + 1, item.owner, 'proof-gap 操作包负责人附件', [item.markdown, item.csv].filter(Boolean).join('; '), `actionCount=${item.actionCount}`, `topPriority=${item.topPriority || 'unknown'};gapIds=${item.gapIds.join('; ') || 'missing'}`, '负责人附件只用于补证派工,不证明客户效果。', item.gapIds.length, `evidenceRowCount=${item.evidenceRowCount}`, `nextActionCount=${item.nextActionCount}` )), ...fillRows.map(item => exportRow('填表顺序', item.order, item.owner, item.title, item.file, item.status, item.acceptance, item.boundary, item.issueCount)), ...proofGapRows.map(item => exportRow('证明缺口', item.priority, item.owner, item.title, item.fillFiles.join('; '), item.status, item.acceptance, item.boundary, '', item.evidence, item.nextAction)), ...repairRows.map((item, index) => exportRow('修复清单', index + 1, item.owner, item.title, item.csv, item.status, item.acceptance, item.boundary, item.issueCount)), ...recheckCommands.map((item, index) => exportRow('复验命令', index + 1, item.owner, item.stage, item.command, '待真实材料补齐后运行', item.acceptance, item.boundary)) ]; const proofOpenCount = Number(summaries.proofGapClosure?.openCount ?? summaries.operatorPack?.openCount ?? summaries.planStatus?.proofOpenCount ?? 0); const operatorPackEvidenceRowCount = Number(summaries.operatorPack?.evidenceRowCount || countRowsWith(proofGapRows, row => row.evidence)); const operatorPackNextActionCount = Number(summaries.operatorPack?.nextActionCount || countRowsWith(proofGapRows, row => row.nextAction)); const operatorPackMissingProofRequirementCount = Number(summaries.operatorPack?.missingProofRequirementCount || proofGapRows.reduce((total, row) => total + row.missingProofRequirements.length, 0)); const operatorPackOwnerArtifactCount = Number(summaries.operatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0); const expectedOperatorPackOwnerArtifactCount = expectedOwnerArtifactCount(summaries.operatorPack, operatorPackOwnerArtifacts); const complete = Boolean( summaries.proofGapClosure?.complete && summaries.planStatus?.complete && summaries.optimizationCompletion?.complete && summaries.optimizationCompletion?.readyForClaim && proofOpenCount === 0 ); const passed = templateFiles.some(item => item.path && item.path.includes('history-data-template.csv')) && templateFiles.some(item => item.path && item.path.includes('manual-review-template.csv')) && templateFiles.some(item => item.path && item.path.includes('video-resource-template.csv')) && templateFiles.some(item => item.path && item.path.includes('long-run-readiness-report.md')) && fillRows.length >= 3 && proofGapRows.length >= 5 && recheckCommands.some(item => item.command.includes('intake:readiness')) && recheckCommands.some(item => item.command.includes('customer-effect:audit')) && recheckCommands.some(item => item.command.includes('acceptance:video-ab')) && recheckCommands.some(item => item.command.includes('longrun:readiness')) && operatorPackEvidenceRowCount >= proofGapRows.length && operatorPackNextActionCount >= proofGapRows.length && operatorPackOwnerArtifactCount >= expectedOperatorPackOwnerArtifactCount && operatorPackOwnerArtifacts.every(item => item.markdown && item.csv && item.evidenceRowCount >= 1 && item.nextActionCount >= 1) && proofGapRows.every(item => item.evidence && item.nextAction) && exportRows.every(item => /不.*证明|不得.*宣称|不能|只证明/.test(item.boundary || '')); return { generatedAt: new Date().toISOString(), root, outputsDir, passed, complete, directCustomerProof: false, proofLevel: 'not_business_proof', proofOpenCount, proofGapRowCount: proofGapRows.length, fillRowCount: fillRows.length, repairRowCount: repairRows.length, recheckCommandCount: recheckCommands.length, exportRowCount: exportRows.length, operatorPackEvidenceRowCount, operatorPackNextActionCount, operatorPackMissingProofRequirementCount, operatorPackOwnerArtifactCount, expectedOperatorPackOwnerArtifactCount, sourceState: { intakeChecklistPassed: Boolean(summaries.intakeChecklist?.passed), intakeFieldItemCount: Number(summaries.intakeChecklist?.itemCount || 0), intakeMissingRequiredCount: Number(summaries.intakeChecklist?.missingRequiredCount || 0), intakePlaceholderCount: Number(summaries.intakeChecklist?.placeholderCount || 0), operatorPackPassed: Boolean(summaries.operatorPack?.passed), operatorPackOpenCount: Number(summaries.operatorPack?.openCount || 0), operatorPackEvidenceRowCount, operatorPackNextActionCount, operatorPackMissingProofRequirementCount, operatorPackOwnerArtifactCount, expectedOperatorPackOwnerArtifactCount, latestFormPassed: Boolean(summaries.latestFormIndex?.passed), planExecutionComplete: Boolean(summaries.planStatus?.complete), optimizationCompletionComplete: Boolean(summaries.optimizationCompletion?.complete), optimizationCompletionReadyForClaim: Boolean(summaries.optimizationCompletion?.readyForClaim), longRunReadinessReady: Boolean(summaries.longRunReadiness?.ready), longRunReadinessFailCount: Number(summaries.longRunReadiness?.counts?.fail ?? summaries.longRunReadiness?.failCount ?? 0), longRunReadinessProofLevel: summaries.longRunReadiness ? 'not_business_proof' : '', longRunReadinessDirectCustomerProof: false, intakeReady: Boolean(summaries.intakeReadiness?.acceptance?.overallReady), videoReady: Boolean(summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight) }, files: { summary: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle-summary.json', report: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle.md', csv: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle.csv', operatorPackByOwnerDir: relIfExists(root, path.join(dirs.operatorPack, 'by-owner')) }, templateFiles, operatorPackOwnerArtifacts, fillRows, proofGapRows, repairRows, recheckCommands, exportRows, guardrails: [ '本总包只把真实材料入口、字段缺口、修复清单和复验命令集中到一个地方,不证明客户效果。', '必须把 example.com、示例、候选、待补、待客户反馈等占位内容替换成真实客户材料。', 'proofOpenCount 不为 0 时,不得宣称提号率提升、客户效果达标或人工补号量下降。', 'optimization-completion.readyForClaim=false 时,不得宣称长期优化完成。', 'longrun:readiness.ready=false 时,不能启动或宣称 full-matrix/live 长跑完成。', '缺真实视频 URL、封面、字幕、ASR、帧图或正文时,不能声明视频分析完成。', '不得在表格、报告、日志或命令中写入 sessionToken、鉴权头、模型 token 或 npm token。' ] }; } function buildTemplateFiles(root, dirs, summaries) { return [ { owner: '商务', title: '历史 Brief 数据模板', path: relIfExists(root, path.join(dirs.dataPack, 'history-data-template.csv')), status: summaries.intakeReadiness?.acceptance?.historyReady ? '已就绪' : '待填真实历史数据', usage: '填写真实历史 Brief、参考账号/视频、人工最终名单、客户选择和历史人工补号量基线。', boundary: '模板存在不证明客户效果。' }, { owner: '商务', title: '人工复核模板', path: relIfExists(root, path.join(dirs.dataPack, 'manual-review-template.csv')), status: summaries.intakeReadiness?.acceptance?.reviewReady ? '已就绪' : '待填人工复核和客户选择', usage: '填写候选人工复核标签、客户选择、负样本归因、反馈原因和本轮人工补号量。', boundary: '复核模板存在不证明客户效果。' }, { owner: '商务/投放', title: '视频资源模板', path: relIfExists(root, path.join(dirs.videoPack, 'video-resource-template.csv')), status: summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight ? '可进入视频 A/B 前置' : '待补真实视频资源', usage: '填写真实参考视频和真实候选视频 URL,并补封面、ASR、帧图、标题或内容摘要证据。', boundary: '视频资源模板存在不证明视频提升提号率。' }, { owner: '商务/投放', title: '字段级补齐核对表', path: relIfExists(root, path.join(dirs.intakeChecklist, 'intake-field-checklist.md')), status: summaries.intakeChecklist?.passed ? `待处理 ${Number(summaries.intakeChecklist.itemCount || 0)} 项` : '缺失或未生成', usage: '按行号、字段、负责人和修复动作逐格补齐真实材料。', boundary: '字段核对表只安排补齐动作,不证明客户效果。' }, { owner: '商务/技术/AI', title: '提号补证操作包', path: relIfExists(root, path.join(dirs.operatorPack, 'proof-gap-operator-pack.md')), status: summaries.operatorPack?.passed ? `openCount=${Number(summaries.operatorPack.openCount || 0)}` : '缺失或未生成', usage: `把五类真实证明缺口转成负责人、当前证据、下一步、填写文件、待补字段、复验命令和通过标准;当前 evidenceRowCount=${Number(summaries.operatorPack?.evidenceRowCount || 0)},nextActionCount=${Number(summaries.operatorPack?.nextActionCount || 0)}。`, boundary: '操作包只安排补证,不证明客户效果。' }, { owner: '商务', title: '人工复核标签与归因指南', path: relIfExists(root, path.join(dirs.manualReviewGuide, 'manual-review-label-guide.md')), status: summaries.manualReviewGuide?.passed ? `标签 ${Number(summaries.manualReviewGuide.labelCount || 0)} 个` : '缺失或未生成', usage: '规范商务复核标签、负样本归因和客户反馈填写口径。', boundary: '标签指南只统一口径,不证明客户效果。' }, { owner: '商务/技术/AI', title: '商务执行总表', path: relIfExists(root, path.join(dirs.businessExecution, 'business-execution-index.md')), status: summaries.businessExecution?.passed ? `rowCount=${Number(summaries.businessExecution.rowCount || 0)}` : '缺失或未生成', usage: '在一张表里查看负责人行动、修复清单、字段补齐负责人和复验命令。', boundary: '执行总表只组织工作流,不证明客户效果。' }, { owner: '商务/技术/AI', title: '最新提号表单索引', path: relIfExists(root, path.join(dirs.latestFormIndex, 'latest-form-index.md')), status: summaries.latestFormIndex?.passed ? '已索引' : '缺失或未生成', usage: '快速定位最新补证表单、软件端候选名单、负责人行动文件和中文 SOP。', boundary: '索引入口不证明客户效果。' }, { owner: '技术/AI', title: '优化完成度审计报告', path: relIfExists(root, path.join(dirs.optimizationCompletion, 'optimization-completion-report.md')), status: summaries.optimizationCompletion ? `readyForClaim=${Boolean(summaries.optimizationCompletion.readyForClaim)},proofGapOpenCount=${Number(summaries.optimizationCompletion.proofGapOpenCount || 0)}` : '缺失或未生成', usage: '复查长期优化是否具备完成声明条件,并汇总仍阻塞的真实证明缺口。', boundary: '完成度审计入口本身不证明客户效果;readyForClaim=false 时不得宣称长期优化完成。' }, { owner: '技术/AI', title: '长跑前置门禁报告', path: relIfExists(root, path.join(dirs.longRunReadiness, 'long-run-readiness-report.md')), status: summaries.longRunReadiness ? `ready=${Boolean(summaries.longRunReadiness.ready)},failCount=${Number(summaries.longRunReadiness?.counts?.fail ?? summaries.longRunReadiness?.failCount ?? 0)}` : '缺失或未生成', usage: '复查 full-matrix/live 长跑是否具备启动条件,并定位 live preflight、历史数据、intake readiness 和 proof-gap closure 卡点。', boundary: '长跑前置门禁入口不证明客户效果;ready=false 时不能启动或宣称 full-matrix/live 长跑完成。' } ]; } function buildFillRows(summaries) { const indexedRows = summaries.latestFormIndex?.businessFillGuide?.rows; const rows = Array.isArray(indexedRows) && indexedRows.length ? indexedRows : (summaries.intakeChecklist?.ownerGroups || []).map((group, index) => ({ order: index + 1, owner: group.owner, section: group.section, file: (group.files || [])[0] || '', firstItemIds: group.firstItemIds || [], issueCount: Number(group.itemCount || 0), fillAction: group.nextAction || '', recheckCommand: '', acceptance: group.acceptance || '', boundary: group.boundary || '补齐材料,不证明客户效果' })); return rows.map((row, index) => { const section = row.section || ''; const videoReady = section === 'video' && Boolean(summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight) && Number(summaries.videoReadiness?.failureCount || 0) === 0; const issueCount = videoReady ? 0 : Number(row.issueCount || 0); return { order: Number(row.order || index + 1), owner: row.owner || '', section, title: sectionTitle(section), file: normalizePath(row.file || ''), firstItemIds: Array.isArray(row.firstItemIds) ? row.firstItemIds : [], issueCount, status: videoReady ? '可进入视频 A/B 前置' : (issueCount > 0 ? '待补齐' : '待复验'), fillAction: videoReady ? '保持真实参考视频和真实候选视频证据链,下一步进入真实视频 A/B live proof。' : (row.fillAction || ''), recheckCommand: row.recheckCommand || '', acceptance: row.acceptance || '', boundary: row.boundary || '补齐材料,不证明客户效果。' }; }); } function buildProofGapRows(operatorPack, closure) { const closureRows = new Map((Array.isArray(closure?.rows) ? closure.rows : []).map(row => [row.id, row])); return (Array.isArray(operatorPack?.rows) ? operatorPack.rows : []).map(row => { const closureRow = closureRows.get(row.gapId) || {}; const status = closureRow.status || row.status || 'open'; const missingProofRequirements = Array.isArray(closureRow.missingProofRequirements) ? closureRow.missingProofRequirements : (Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements : []); return { gapId: row.gapId || '', title: row.title || closureRow.title || '', owner: row.owner || '', priority: Number(row.priority || 99), status, fillFiles: Array.isArray(row.fillFiles) ? row.fillFiles.map(normalizePath) : [], fields: Array.isArray(row.fields) ? row.fields : [], recheckCommands: Array.isArray(row.recheckCommands) ? row.recheckCommands : [], evidence: closureRow.evidence || row.evidence || '', missingProofRequirements, nextAction: status === 'closed' ? (closureRow.next || '保持证据链并纳入 evidence:index。') : (row.nextAction || closureRow.next || ''), acceptance: row.acceptance || closureRow.required || '', boundary: row.boundary || '补齐材料,不证明客户效果。' }; }).sort((a, b) => a.priority - b.priority); } function expectedOwnerArtifactCount(operatorPack, ownerArtifacts) { if (Array.isArray(operatorPack?.ownerArtifacts) && operatorPack.ownerArtifacts.length) { return operatorPack.ownerArtifacts.length; } if (Number.isFinite(Number(operatorPack?.ownerArtifactCount)) && Number(operatorPack.ownerArtifactCount) > 0) { return Number(operatorPack.ownerArtifactCount); } if (Array.isArray(operatorPack?.ownerGroups) && operatorPack.ownerGroups.length) { return operatorPack.ownerGroups.length; } if (Array.isArray(ownerArtifacts) && ownerArtifacts.length) return ownerArtifacts.length; return 1; } function normalizeOperatorPackOwnerArtifacts(root, operatorPackDir, operatorPack) { if (!Array.isArray(operatorPack?.ownerArtifacts)) return []; return operatorPack.ownerArtifacts.map(item => ({ owner: item.owner || '', actionCount: Number(item.actionCount || 0), topPriority: Number(item.topPriority || 0), markdown: relIfExists(root, resolveArtifactPath(root, operatorPackDir, item.markdown)), csv: relIfExists(root, resolveArtifactPath(root, operatorPackDir, item.csv)), gapIds: Array.isArray(item.gapIds) ? item.gapIds : [], evidenceRowCount: Number(item.evidenceRowCount || 0), nextActionCount: Number(item.nextActionCount || 0) })).filter(item => item.owner || item.markdown || item.csv); } function resolveArtifactPath(root, baseDir, value) { if (!value) return ''; const text = String(value); const normalized = text.replace(/\\/g, '/'); if (path.isAbsolute(text)) return text; if (normalized.startsWith('outputs/')) return path.join(root, normalized); return path.join(baseDir, text); } function buildRepairRows(root, dirs, summaries) { return [ { owner: '商务', title: '历史数据/人工复核修复清单', csv: relIfExists(root, path.join(dirs.intakeReadiness, 'intake-readiness-repair-actions.csv')), issueCount: Number(summaries.intakeReadiness?.failureCount || 0), actionCount: Array.isArray(summaries.intakeReadiness?.repairActions) ? summaries.intakeReadiness.repairActions.length : 0, status: summaries.intakeReadiness?.acceptance?.overallReady ? '已就绪' : '待补齐', acceptance: 'intake:readiness overallReady=true 且 failureCount=0。', boundary: '修复清单只帮助补齐材料,不证明客户效果。' }, { owner: '商务/投放', title: '视频资源修复清单', csv: relIfExists(root, path.join(dirs.videoReadiness, 'video-resource-repair-actions.csv')), issueCount: Number(summaries.videoReadiness?.failureCount || 0), actionCount: Array.isArray(summaries.videoReadiness?.repairActions) ? summaries.videoReadiness.repairActions.length : 0, status: summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight ? '可进入视频 A/B 前置' : '待补视频资源', acceptance: 'video:resource-readiness readyForVideoAbPreflight=true 且 failureCount=0。', boundary: '视频修复清单不证明视频分析提升提号率。' }, { owner: '商务/投放', title: '主页近期内容证据修复清单', csv: relIfExists(root, path.join(dirs.homepageReadiness, 'homepage-evidence-repair-actions.csv')), issueCount: Number(summaries.homepageReadiness?.failureCount || 0), actionCount: Array.isArray(summaries.homepageReadiness?.repairActions) ? summaries.homepageReadiness.repairActions.length : 0, status: summaries.homepageReadiness?.ready ? '主页证据已就绪' : '待补主页证据', acceptance: 'homepage:readiness ready=true,且强推荐具备近期内容证据。', boundary: '主页证据修复清单不证明客户效果。' } ]; } function buildRecheckCommands(summaries) { const commands = []; for (const item of summaries.intakeChecklist?.recheckCommands || []) { commands.push({ stage: item.stage || '', owner: item.owner || '技术/AI', command: item.command || '', acceptance: item.acceptance || '', boundary: item.boundary || '复验通过不等于客户效果证明。' }); } for (const row of summaries.operatorPack?.rows || []) { for (const command of row.recheckCommands || []) { commands.push({ stage: row.title || row.gapId || '证明缺口复验', owner: row.owner || '技术/AI', command, acceptance: row.acceptance || '', boundary: row.boundary || '复验通过不等于客户效果证明。' }); } } commands.push( { stage: '长跑前置门禁', owner: '技术/AI', command: 'npm run longrun:readiness -- --mode full-matrix --intake-readiness outputs\\intake-readiness-latest\\intake-readiness-summary.json --proof-gap outputs\\proof-gap-request-latest\\proof-gap-request-summary.json --proof-gap-closure outputs\\proof-gap-closure-latest\\proof-gap-closure-summary.json --output outputs\\long-run-readiness-latest', acceptance: 'ready=true 且 failCount=0 后,才允许进入真实 live/provider 长跑启动判断。', boundary: 'ready=false 时不能启动或宣称 full-matrix/live 长跑完成。' }, { stage: '优化完成度审计', owner: '技术/AI', command: 'npm run optimization:completion -- --output outputs\\optimization-completion-latest', acceptance: 'complete=true、readyForClaim=true、proofGapOpenCount=0 才能进入完成判断。', boundary: 'readyForClaim=false 时不得宣称长期优化完成。' }, { stage: '真实材料一键审计 Pipeline', owner: '技术/AI', 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', acceptance: '真实历史数据、视频资源、复核指标和客户效果审计按 pipeline 通过。', boundary: 'pipeline 未关闭 proof-gap:closure 前不得宣称客户效果。' }, { stage: '每轮沉淀刷新', owner: '技术/AI', command: 'npm run round:refresh -- --output-root outputs --strict', acceptance: 'round-deposition.passed=true 且 failCount=0。', boundary: '刷新沉淀不证明客户效果。' } ); return dedupeBy(commands.filter(item => item.command), item => item.command); } function exportRow(type, order, owner, title, target, status, acceptance, boundary, issueCount = '', evidence = '', nextAction = '') { return { type, order, owner: owner || '', title: title || '', target: normalizePath(target || ''), status: status || '', issueCount, evidence: evidence || '', nextAction: nextAction || '', acceptance: acceptance || '', boundary: boundary || '不证明客户效果。' }; } function renderReport(summary) { const lines = [ '# 真实验收材料收集总包', '', `- 生成时间:${summary.generatedAt}`, `- 是否完成长期目标:${summary.complete ? '是' : '否'}`, `- proofOpenCount:${summary.proofOpenCount}`, `- operatorPackEvidenceRowCount:${summary.operatorPackEvidenceRowCount}`, `- operatorPackNextActionCount:${summary.operatorPackNextActionCount}`, `- operatorPackMissingProofRequirementCount:${summary.operatorPackMissingProofRequirementCount}`, `- operatorPackOwnerArtifactCount:${summary.operatorPackOwnerArtifactCount}`, `- 用途:把真实材料模板、字段缺口、修复清单、证明缺口和复验命令集中成一个商务可打开的入口;不证明客户效果。`, '', '## 打开顺序', '', '| 负责人 | 文件 | 状态 | 用法 | 边界 |', '| --- | --- | --- | --- | --- |', ...summary.templateFiles.map(item => tableRow([item.owner, item.path || '缺失', item.status, item.usage, item.boundary])), '', '## 操作包负责人附件', '', '| 负责人 | 动作数 | Markdown | CSV | 缺口 | 当前证据行 | 下一步动作 |', '| --- | ---: | --- | --- | --- | ---: | ---: |', ...summary.operatorPackOwnerArtifacts.map(item => tableRow([ item.owner, item.actionCount, item.markdown || '缺失', item.csv || '缺失', item.gapIds.join(';') || '缺失', item.evidenceRowCount, item.nextActionCount ])), '', '## 商务填表顺序', '', '| 顺序 | 负责人 | 区域 | 文件 | 优先项 | 问题数 | 填表动作 | 补齐后复验 | 验收 | 边界 |', '| ---: | --- | --- | --- | --- | ---: | --- | --- | --- | --- |', ...summary.fillRows.map(row => tableRow([ row.order, row.owner, row.title, row.file || '缺失', row.firstItemIds.join('、') || '无', row.issueCount, row.fillAction, row.recheckCommand || '补齐后运行 round:refresh', row.acceptance, row.boundary ])), '', '## 证明缺口闭环', '', '| 优先级 | 缺口 | 负责人 | 状态 | 填写文件 | 当前证据 | 下一步 | 待补字段/证明 | 通过标准 | 边界 |', '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...summary.proofGapRows.map(row => tableRow([ row.priority, `${row.title} (${row.gapId})`, row.owner, row.status, row.fillFiles.join(';') || '缺失', row.evidence || '缺失', row.nextAction || '按操作包补齐', row.fields.join(';') || '按操作包补齐', row.acceptance, row.boundary ])), '', '## 修复清单入口', '', '| 负责人 | 修复清单 | 当前问题数 | 修复动作数 | 状态 | 验收 | 边界 |', '| --- | --- | ---: | ---: | --- | --- | --- |', ...summary.repairRows.map(row => tableRow([row.owner, row.csv || '缺失', row.issueCount, row.actionCount, row.status, row.acceptance, row.boundary])), '', '## 补齐后复验命令', '', '| 阶段 | 负责人 | 命令 | 验收 | 边界 |', '| --- | --- | --- | --- | --- |', ...summary.recheckCommands.map(item => tableRow([item.stage, item.owner, item.command, item.acceptance, item.boundary])), '', '## 防误用边界', '', ...summary.guardrails.map(item => `- ${item}`) ]; return lines.join('\n'); } function renderCsv(rows) { const headers = ['类型', '顺序', '负责人', '标题', '文件或命令', '状态', '问题数', '当前证据', '下一步', '验收标准', '边界']; return [ headers.join(','), ...rows.map(row => [ row.type, row.order, row.owner, row.title, row.target, row.status, row.issueCount, row.evidence, row.nextAction, row.acceptance, row.boundary ].map(csvCell).join(',')) ].join('\n'); } function sectionTitle(section) { if (section === 'history') return '历史 Brief 数据'; if (section === 'review') return '人工复核和客户选择'; if (section === 'video') return '视频资源证据'; return section || '真实材料'; } function dedupeBy(items, keyFn) { const seen = new Set(); const result = []; for (const item of items) { const key = keyFn(item); if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; } function countRowsWith(rows, predicate) { return rows.reduce((count, row) => count + (predicate(row) ? 1 : 0), 0); } function pickDir(outputsDir, pattern) { if (!fs.existsSync(outputsDir)) return ''; const latest = fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory() && pattern.test(entry.name)) .map(entry => path.join(outputsDir, entry.name)) .filter(fs.existsSync) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0]; return latest || ''; } function readJson(file) { if (!file || !fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; } } function relIfExists(root, file) { return file && fs.existsSync(file) ? path.relative(root, file).replace(/\\/g, '/') : ''; } function normalizePath(value) { return String(value || '').replace(/\\/g, '/'); } function tableRow(cells) { return `| ${cells.map(cell => String(cell ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ')).join(' | ')} |`; } function csvCell(value) { const text = String(value ?? '').replace(/\r?\n/g, ' '); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function parseArgs(argv) { const result = {}; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (!arg.startsWith('--')) continue; const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) result[key] = true; else { result[key] = next; index += 1; } } return result; } function withBom(text) { return `\uFEFF${text}`; } if (require.main === module) main(); module.exports = { buildRealProofIntakeBundle };