#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'proof-gap-operator-pack-latest'); function main() { const args = parseArgs(process.argv.slice(2)); const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs')); const outputDir = path.resolve(args.output || DEFAULT_OUTPUT); const summary = buildOperatorPack({ root: ROOT, outputsDir }); fs.mkdirSync(outputDir, { recursive: true }); 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 ownerArtifacts = writeOwnerArtifacts(outputDir, summary.ownerGroups, summary.rows); summary.ownerArtifacts = ownerArtifacts; summary.ownerArtifactCount = ownerArtifacts.length; summary.files.byOwnerDir = 'outputs/proof-gap-operator-pack-latest/by-owner'; fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8'); fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8'); fs.writeFileSync(csvPath, withBom(renderCsv(summary.rows)), 'utf8'); console.log(JSON.stringify({ outputDir, summary: summaryPath, report: reportPath, csv: csvPath, passed: summary.passed, openCount: summary.openCount, rowCount: summary.rowCount }, null, 2)); if (args.strict && !summary.passed) process.exitCode = 1; } function buildOperatorPack({ root, outputsDir }) { const closure = readJson(path.join(pickDir(outputsDir, 'proof-gap-closure-latest', /^proof-gap-closure-/), 'proof-gap-closure-summary.json')); const checklist = readJson(path.join(pickDir(outputsDir, 'intake-field-checklist-latest', /^intake-field-checklist-/), 'intake-field-checklist-summary.json')); const nextActions = readJson(path.join(pickDir(outputsDir, 'business-proof-next-actions-latest', /^business-proof-next-actions-/), 'business-proof-next-actions-summary.json')); const planStatus = readJson(path.join(pickDir(outputsDir, 'plan-execution-status-latest', /^plan-execution-status-/), 'plan-execution-status-summary.json')); const rows = buildRows({ closure, checklist, nextActions }); const openRows = rows.filter(row => row.status !== 'closed'); const ownerGroups = groupByOwner(openRows); const gapIds = rows.map(row => row.gapId).filter(Boolean); const fillFiles = Array.from(new Set(rows.flatMap(row => row.fillFiles || []))); const recheckCommands = Array.from(new Set(rows.flatMap(row => row.recheckCommands || []))); const evidenceRowCount = rows.filter(row => String(row.evidence || '').trim()).length; const nextActionCount = rows.filter(row => String(row.nextAction || '').trim()).length; const missingProofRequirementCount = rows.reduce((sum, row) => sum + (Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements.length : 0), 0); const passed = rows.length >= 5 && openRows.length >= 1 && rows.every(hasRunnableBoundary) && rows.some(row => row.gapId === 'historical-dataset') && rows.some(row => row.gapId === 'video-real-candidate') && rows.some(row => row.gapId === 'manual-review-and-customer-effect'); return { generatedAt: new Date().toISOString(), root, outputsDir, passed, complete: openRows.length === 0, directCustomerProof: false, proofLevel: 'not_business_proof', openCount: openRows.length, rowCount: rows.length, gapIds, fillFileCount: fillFiles.length, recheckCommandCount: recheckCommands.length, ownerGroupCount: ownerGroups.length, ownerArtifactCount: 0, evidenceRowCount, nextActionCount, missingProofRequirementCount, sourceState: { proofGapClosureOpenCount: Number(closure?.openCount || 0), planProofOpenCount: Number(planStatus?.proofOpenCount || 0), intakeFieldItemCount: Number(checklist?.itemCount || 0), nextActionCount: Array.isArray(nextActions?.actions) ? nextActions.actions.length : 0 }, files: { summary: 'outputs/proof-gap-operator-pack-latest/proof-gap-operator-pack-summary.json', report: 'outputs/proof-gap-operator-pack-latest/proof-gap-operator-pack.md', csv: 'outputs/proof-gap-operator-pack-latest/proof-gap-operator-pack.csv', byOwnerDir: 'outputs/proof-gap-operator-pack-latest/by-owner' }, ownerGroups, ownerArtifacts: [], guardrails: [ '本操作包只用于安排补证,不证明客户效果。', '所有模板、example.com、候选、待补内容都必须替换为真实客户材料。', 'proofOpenCount 不为 0 时,不得宣称命中率提升、客户效果达标或人工补号量下降。', '不得在表格、报告、命令或日志中写入 sessionToken、鉴权头、模型 token 或 npm token。' ], rows }; } function hasRunnableBoundary(row) { return Boolean(row.owner) && Array.isArray(row.fillFiles) && row.fillFiles.length > 0 && Array.isArray(row.recheckCommands) && row.recheckCommands.length > 0 && Boolean(String(row.evidence || '').trim()) && Boolean(String(row.nextAction || '').trim()) && Boolean(row.acceptance) && /不.*证明|不得宣称|不等同于/.test(String(row.boundary || '')); } function buildRows({ closure, checklist, nextActions }) { const closureRows = Array.isArray(closure?.rows) ? closure.rows : []; const actionByGap = new Map((Array.isArray(nextActions?.actions) ? nextActions.actions : []).map(action => [ normalizeActionGapId(action), action ])); const fieldMap = fieldsBySection(checklist); const known = [ { gapId: 'historical-dataset', title: '真实历史 Brief 数据集', owner: '商务', priority: 1, fillFiles: ['outputs/data-intake-pack-latest/history-data-template.csv'], fields: fieldsFor(fieldMap, 'history', ['客户原始 Brief', '参考账号或视频', '博主名称', '主页链接', '客户选择', '历史人工补号量基线']), recheckCommands: [ 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest', 'npm run history:from-csv -- --input <历史数据CSV> --output ', 'npm run history:audit -- --input --output <历史审计输出目录> --strict' ], acceptance: 'historyReady=true;真实历史 Brief 数 >=5;readyForCustomerEffectProof=true', boundary: '补齐历史材料不证明客户效果。' }, { gapId: 'video-real-candidate', title: '真实候选视频资源', owner: '商务/投放', priority: 2, fillFiles: ['outputs/video-intake-pack-latest/video-resource-template.csv'], fields: fieldsFor(fieldMap, 'video', ['资源角色', '博主名称', '主页链接', '视频链接', '标题', '内容摘要', '风格调性标签']), recheckCommands: [ 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict' ], acceptance: 'readyForVideoAbPreflight=true;failureCount=0;至少有真实参考视频和真实候选视频。', boundary: '补齐视频资源只证明 A/B 前置资源齐备,不证明客户效果。' }, { gapId: 'video-ab-live-proof', title: '真实视频 A/B live proof', owner: '技术/AI', priority: 3, fillFiles: ['outputs/video-intake-pack-latest/video-resource-template.csv'], fields: ['proofContext.mode=live', 'proofContext.collectionMode=live', 'requiresVocSocialProvider=true', 'requiresVideoAnalysisProvider=true'], recheckCommands: ['npm run acceptance:video-ab'], acceptance: '真实 provider、真实参考视频和真实候选视频下通过;proofContext 标记为 live。', boundary: 'sample 或接口 200 不证明视频分析提升提号率。' }, { gapId: 'live-provider-overnight-proof', title: '真实 live/provider 长跑证明', owner: '技术/AI', priority: 4, fillFiles: ['outputs/data-intake-pack-latest/history-data-template.csv', 'outputs/video-intake-pack-latest/video-resource-template.csv'], fields: ['liveEnabled=true', 'overallPass=true', 'failureCount=0', 'failedGateCount=0'], recheckCommands: [ 'npm run longrun:readiness -- --mode full-matrix --output outputs\\long-run-readiness-latest', 'npm run overnight:quality' ], acceptance: 'longrun ready=true;overnight aggregate liveEnabled=true、overallPass=true、failureCount=0。', boundary: '长跑通过只证明 provider/live 流程稳定,不单独证明客户效果。' }, { gapId: 'manual-review-and-customer-effect', title: '商务复核和客户效果证明', owner: '商务', priority: 5, fillFiles: ['outputs/data-intake-pack-latest/manual-review-template.csv'], fields: fieldsFor(fieldMap, 'review', ['客户选择', '归因类型', '反馈原因', '本轮人工补号量', '历史人工补号量基线']), recheckCommands: [ 'npm run review:metrics -- --input <已标注CSV> --output <复核指标输出目录> --strict', 'npm run customer-effect:audit -- --review-csv <已标注CSV> --history-audit --current-manual-supplement-count <本轮人工补号量> --output <客户效果输出目录> --strict' ], acceptance: 'review:metrics overallPass=true;customer-effect:audit overallPass=true。', boundary: '缺客户选择、历史基线或本轮人工补号量时,不得宣称客户效果。' } ]; return known.map(item => { const closureRow = closureRows.find(row => row.id === item.gapId); const action = actionByGap.get(item.gapId); const status = closureRow?.status || action?.status || 'open'; return { ...item, primaryFillFile: item.fillFiles[0] || '', primaryRecheckCommand: item.recheckCommands[0] || '', status, evidence: closureRow?.evidence || action?.evidence || '', missingProofRequirements: Array.isArray(closureRow?.missingProofRequirements) ? closureRow.missingProofRequirements : [], nextAction: status === 'closed' ? (closureRow?.next || '保持证据链并纳入 evidence:index。') : (action?.next || closureRow?.next || item.recheckCommands[0]) }; }); } function normalizeActionGapId(action) { const id = String(action?.id || ''); if (id.includes('video-ab-live-proof')) return 'video-ab-live-proof'; if (id.includes('live-provider-overnight-proof')) return 'live-provider-overnight-proof'; const title = String(action?.title || ''); if (/历史|Brief/i.test(title)) return 'historical-dataset'; if (/视频资源|候选视频/i.test(title)) return 'video-real-candidate'; if (/复核|客户效果/i.test(title)) return 'manual-review-and-customer-effect'; return id; } function fieldsBySection(checklist) { const result = {}; for (const item of Array.isArray(checklist?.items) ? checklist.items : []) { const section = item.section || 'unknown'; if (!result[section]) result[section] = []; const field = item.field || item.issueType || ''; if (field && !result[section].includes(field)) result[section].push(field); } return result; } function fieldsFor(fieldMap, section, fallback) { const fields = fieldMap[section] || []; return fields.length ? fields.slice(0, 8) : fallback; } function groupByOwner(rows) { const groups = new Map(); for (const row of rows) { if (!groups.has(row.owner)) groups.set(row.owner, []); groups.get(row.owner).push(row); } return Array.from(groups.entries()).map(([owner, items]) => ({ owner, actionCount: items.length, topPriority: Math.min(...items.map(item => Number(item.priority || 99))), fillFiles: Array.from(new Set(items.flatMap(item => item.fillFiles))), recheckCommands: Array.from(new Set(items.flatMap(item => item.recheckCommands))), gapIds: items.map(item => item.gapId) })); } function renderReport(summary) { return [ '# 提号补证操作包', '', `- 生成时间:${summary.generatedAt}`, `- 是否完成:${summary.complete ? '是' : '否'}`, `- 证明缺口 openCount:${summary.openCount}`, `- 行数:${summary.rowCount}`, `- 缺口 ID:${summary.gapIds.join(';')}`, `- 填写文件数:${summary.fillFileCount}`, `- 复验命令数:${summary.recheckCommandCount}`, `- 当前证据行数:${summary.evidenceRowCount}`, `- 下一步动作数:${summary.nextActionCount}`, `- 缺失证明要求数:${summary.missingProofRequirementCount}`, '- 用途:把真实补证缺口压成负责人、填写文件、字段、复验命令和通过标准;不证明客户效果。', '', '## 负责人视图', '', '| 负责人 | 动作数 | 最高优先级 | 填写文件 | 复验命令 | 缺口 |', '| --- | ---: | ---: | --- | --- | --- |', ...summary.ownerGroups.map(group => tableRow([ group.owner, group.actionCount, group.topPriority, group.fillFiles.join('; '), group.recheckCommands.join('; '), group.gapIds.join('; ') ])), '', '## 负责人文件', '', '| 负责人 | 动作数 | 最高优先级 | Markdown | CSV | 缺口 |', '| --- | ---: | ---: | --- | --- | --- |', ...summary.ownerArtifacts.map(item => tableRow([ item.owner, item.actionCount, item.topPriority, item.markdown, item.csv, item.gapIds.join('; ') ])), '', '## 缺口操作表', '', '| 优先级 | 缺口 | 负责人 | 状态 | 当前证据 | 填写文件 | 待补字段/证明 | 下一步 | 复验命令 | 通过标准 | 边界 |', '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...summary.rows.map(row => tableRow([ row.priority, `${row.title} (${row.gapId})`, row.owner, row.status, row.evidence, row.fillFiles.join('; '), uniqueList(row.fields.concat(row.missingProofRequirements || [])).join('; '), row.nextAction, row.recheckCommands.join('; '), row.acceptance, row.boundary ])), '', '## 防误报边界', '', ...summary.guardrails.map(item => `- ${item}`) ].join('\n'); } function writeOwnerArtifacts(outputDir, ownerGroups, rows) { const ownerDir = path.join(outputDir, 'by-owner'); fs.mkdirSync(ownerDir, { recursive: true }); const artifacts = []; for (const group of ownerGroups) { const ownerRows = rows.filter(row => row.owner === group.owner); const slug = slugOwner(group.owner); const markdownRel = path.join('by-owner', `${slug}.md`).replace(/\\/g, '/'); const csvRel = path.join('by-owner', `${slug}.csv`).replace(/\\/g, '/'); fs.writeFileSync(path.join(outputDir, markdownRel), withBom(renderOwnerReport(group, ownerRows)), 'utf8'); fs.writeFileSync(path.join(outputDir, csvRel), withBom(renderCsv(ownerRows)), 'utf8'); artifacts.push({ owner: group.owner, actionCount: group.actionCount, topPriority: group.topPriority, markdown: markdownRel, csv: csvRel, gapIds: group.gapIds, evidenceRowCount: ownerRows.filter(row => String(row.evidence || '').trim()).length, nextActionCount: ownerRows.filter(row => String(row.nextAction || '').trim()).length }); } return artifacts; } function renderOwnerReport(group, rows) { return [ `# ${group.owner}提号补证操作包`, '', `- 动作数:${group.actionCount}`, `- 最高优先级:${group.topPriority}`, `- 缺口:${group.gapIds.join(';')}`, '- 用途:只展示归属本负责人的真实证明缺口、当前证据、下一步、填写文件和复验命令;不证明客户效果。', '', '## 操作明细', '', '| 优先级 | 缺口 | 状态 | 当前证据 | 填写文件 | 待补字段/证明 | 下一步 | 复验命令 | 通过标准 | 边界 |', '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- |', ...rows.map(row => tableRow([ row.priority, `${row.title} (${row.gapId})`, row.status, row.evidence, row.fillFiles.join('; '), uniqueList(row.fields.concat(row.missingProofRequirements || [])).join('; '), row.nextAction, row.recheckCommands.join('; '), row.acceptance, row.boundary ])), '', '## 边界', '', '- 本文件只用于按负责人补证派工,不证明提号率提升、客户效果达标或人工补号量下降。', '- 不要在补证材料、命令、日志或报告中写入 sessionToken、Authorization、模型 token 或 npm token。' ].join('\n'); } function renderCsv(rows) { const headers = ['优先级', '缺口ID', '缺口', '负责人', '状态', '当前证据', '主填写文件', '首个复验命令', '填写文件', '待补字段/证明', '下一步', '复验命令', '通过标准', '边界']; return [ headers.join(','), ...rows.map(row => [ row.priority, row.gapId, row.title, row.owner, row.status, row.evidence, row.primaryFillFile, row.primaryRecheckCommand, row.fillFiles.join('; '), uniqueList(row.fields.concat(row.missingProofRequirements || [])).join('; '), row.nextAction, row.recheckCommands.join('; '), row.acceptance, row.boundary ].map(csvCell).join(',')) ].join('\n'); } function pickDir(outputsDir, latestName, pattern) { const latest = path.join(outputsDir, latestName); if (fs.existsSync(latest)) return latest; if (!fs.existsSync(outputsDir)) return latest; const dirs = fs.readdirSync(outputsDir, { withFileTypes: true }) .filter(entry => entry.isDirectory() && pattern.test(entry.name)) .map(entry => path.join(outputsDir, entry.name)) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs); return dirs[0] || latest; } function readJson(file) { if (!fs.existsSync(file)) return null; return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } function tableRow(cells) { return `| ${cells.map(escapeCell).join(' | ')} |`; } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' '); } function uniqueList(values) { return Array.from(new Set((values || []).map(value => String(value || '').trim()).filter(Boolean))); } function slugOwner(owner) { const map = { '商务': 'business', '投放/商务': 'media-business', '商务/投放': 'business-media', '技术/AI': 'tech-ai' }; if (map[owner]) return map[owner]; const ascii = String(owner || 'owner').replace(/[^\w]+/g, '-').replace(/^-|-$/g, '').toLowerCase(); return ascii || Buffer.from(String(owner || 'owner')).toString('hex').slice(0, 12); } function csvCell(value) { const text = String(value ?? ''); return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function parseArgs(argv) { const args = {}; for (let index = 0; index < argv.length; index += 1) { const raw = argv[index]; if (!raw.startsWith('--')) continue; const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; index += 1; } } return args; } function withBom(text) { return `\uFEFF${text}`; } if (require.main === module) main(); module.exports = { buildOperatorPack };