#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'plan-execution-status-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 = buildPlanExecutionStatus({ root: ROOT, outputsDir }); fs.mkdirSync(outputDir, { recursive: true }); const summaryPath = path.join(outputDir, 'plan-execution-status-summary.json'); const reportPath = path.join(outputDir, 'plan-execution-status.md'); const csvPath = path.join(outputDir, 'plan-execution-status.csv'); 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, complete: summary.complete, rowCount: summary.rowCount, proofOpenCount: summary.proofOpenCount }, null, 2)); if (args.strict && !summary.passed) process.exitCode = 1; } function buildPlanExecutionStatus({ root, outputsDir }) { const planPath = path.join(root, 'docs', 'tihao-experience-optimization-plan.md'); const planText = readText(planPath); const status = readJson(path.join(pickDir(outputsDir, 'optimization-status-latest', /^optimization-status-/), 'optimization-status-summary.json')); const closure = readJson(path.join(pickDir(outputsDir, 'proof-gap-closure-latest', /^proof-gap-closure-/), 'proof-gap-closure-summary.json')); const progress = readJson(path.join(pickDir(outputsDir, 'business-proof-progress-latest', /^business-proof-progress-/), 'business-proof-progress-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 latestForm = readJson(path.join(pickDir(outputsDir, 'latest-form-index-latest', /^latest-form-index-/), 'latest-form-index-summary.json')); const handoff = readJson(path.join(pickDir(outputsDir, 'optimization-handoff-latest', /^optimization-handoff-/), 'optimization-handoff-summary.json')); const round = readJson(path.join(pickDir(outputsDir, 'round-deposition-latest', /^round-deposition-/), 'round-deposition-summary.json')); const evidence = readJson(path.join(pickDir(outputsDir, 'evidence-index-latest', /^evidence-index-/), 'evidence-index-summary.json')); const proofGapOperatorPack = readJson(path.join(pickDir(outputsDir, 'proof-gap-operator-pack-latest', /^proof-gap-operator-pack-/), 'proof-gap-operator-pack-summary.json')); const businessExecutionIndex = readJson(path.join(pickDir(outputsDir, 'business-execution-index-latest', /^business-execution-index-/), 'business-execution-index-summary.json')); const proofOpenCount = Number(closure?.openCount ?? latestForm?.nextActions?.proofOpenCount ?? 0); const statusCounts = status?.counts || {}; const progressCounts = progress?.counts || {}; const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts( businessExecutionIndex?.operatorPackOwnerArtifacts || proofGapOperatorPack?.ownerArtifacts || [] ); const operatorPackOwnerArtifactCount = Number(proofGapOperatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0); const rows = buildRows({ planText, status, closure, progress, nextActions, latestForm, handoff, round, evidence, proofGapOperatorPack, businessExecutionIndex, operatorPackOwnerArtifactCount, proofOpenCount }); const counts = countStatuses(rows); return { generatedAt: new Date().toISOString(), root, outputsDir, plan: rel(root, planPath), passed: rows.every(row => row.status !== 'missing'), complete: proofOpenCount === 0 && Boolean(closure?.complete) && Boolean(status?.complete), rowCount: rows.length, proofOpenCount, statusCounts, progressCounts, roundPassed: Boolean(round?.passed), roundPassCount: Number(round?.counts?.pass || 0), roundFailCount: Number(round?.counts?.fail || 0), evidenceCounts: evidence?.counts || {}, nextActionCount: Array.isArray(nextActions?.actions) ? nextActions.actions.length : 0, topNextAction: nextActions?.actions?.[0]?.title || '', operatorPackOwnerArtifactCount, operatorPackOwnerArtifacts, businessExecutionOperatorPackOwnerArtifactCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactCount || 0), businessExecutionOperatorPackOwnerArtifactRowCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0), files: { summary: 'outputs/plan-execution-status-latest/plan-execution-status-summary.json', report: 'outputs/plan-execution-status-latest/plan-execution-status.md', csv: 'outputs/plan-execution-status-latest/plan-execution-status.csv' }, guardrails: [ '本面板只同步计划执行进度,不证明客户效果。', 'proofOpenCount 不为 0 时,不得宣称命中率提升、客户效果达标或人工补号量下降。', 'sample、smoke、provider fallback、接口 200 只能证明流程或接口可用,不能证明业务效果。', '不得在报告、表格、日志或命令中写入 sessionToken、Authorization、模型 token 或 npm token。' ], rows }; } function buildRows(ctx) { const planReadable = hasAll(ctx.planText, ['核心判断', '关键差距', '质量验收标准', '每轮沉淀']); const releasePassed = Number(ctx.status?.counts?.passed || 0) >= 20; const proofOpen = Number(ctx.proofOpenCount || 0); const nextActionCount = Array.isArray(ctx.nextActions?.actions) ? ctx.nextActions.actions.length : 0; const latestFormReady = Boolean(ctx.latestForm?.passed); const roundPassed = Boolean(ctx.round?.passed) && Number(ctx.round?.counts?.fail || 0) === 0; const labelGuideReady = Boolean(ctx.latestForm?.manualReviewLabelGuide?.negativeLabelsRequireAttribution); const evidenceReady = Number(ctx.evidence?.total || 0) > 0; const handoffReady = ctx.handoff && ctx.handoff.complete === false && Number(ctx.handoff.externalBlockers?.length || 0) >= 1; return [ row({ order: 1, section: '核心判断', status: planReadable ? 'passed' : 'missing', evidence: planReadable ? '主计划已是中文可读,并保留核心判断、差距、验收和每轮沉淀。' : '主计划缺少关键中文段落。', currentArtifact: 'docs/tihao-experience-optimization-plan.md', nextAction: '持续把真实业务反馈转成规则、权重或工作流变更。', acceptance: '计划文件中文可读;不含乱码;不宣称未证明效果。', boundary: '计划可读不等于业务效果已证明。' }), row({ order: 2, section: '当前已具备能力', status: releasePassed ? 'passed' : 'missing', evidence: `optimization-status passed=${Number(ctx.status?.counts?.passed || 0)},ready_not_proven=${Number(ctx.status?.counts?.ready_not_proven || 0)},blocked=${Number(ctx.status?.counts?.blocked_by_external_data || 0)}。`, currentArtifact: 'outputs/optimization-status-latest/optimization-status-summary.json', nextAction: '发布或交付前继续运行 npm run acceptance。', acceptance: '基础链路、软件表、硬性规则、反馈闭环、证据台账等能力有脚本或 smoke 保护。', boundary: '能力就绪项多为 smoke/local 证明,不能替代客户效果审计。' }), row({ order: 3, section: '关键差距', status: proofOpen > 0 ? 'open' : 'passed', evidence: `proof-gap-closure.openCount=${proofOpen},topNextAction=${ctx.nextActions?.actions?.[0]?.title || '无'},operatorPackOwnerArtifactCount=${Number(ctx.operatorPackOwnerArtifactCount || 0)}。`, currentArtifact: 'outputs/proof-gap-closure-latest/proof-gap-closure-summary.json', nextAction: ctx.nextActions?.actions?.[0]?.command || '按 proof-gap-closure 的 open rows 补齐真实证据。', acceptance: 'openCount=0 且 status.complete=true 后,才进入完成判断。', boundary: 'openCount>0 时不得宣称长期优化目标完成。' }), row({ order: 4, section: '长跑优化任务', status: nextActionCount > 0 ? 'open' : 'passed', evidence: `business-proof-next-actions.actionCount=${nextActionCount};business-proof-progress.complete=${Boolean(ctx.progress?.complete)},businessExecutionOperatorPackOwnerArtifactRowCount=${Number(ctx.businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0)}。`, currentArtifact: 'outputs/business-proof-next-actions-latest/business-proof-next-actions-report.md', nextAction: ctx.nextActions?.actions?.[0]?.title ? `优先处理:${ctx.nextActions.actions[0].title}` : '继续运行策略矩阵并沉淀结果。', acceptance: '真实历史数据、视频资源、复核指标和客户效果审计形成闭环。', boundary: '当前行动队列是补证任务,不是效果证明。' }), row({ order: 5, section: '软件端表格验收', status: latestFormReady ? 'passed' : 'open', evidence: `latest-form-index.passed=${Boolean(ctx.latestForm?.passed)},primaryForm=${ctx.latestForm?.primaryForm?.csv || 'missing'},clientList=${ctx.latestForm?.clientList?.csv || 'missing'}。`, currentArtifact: 'outputs/latest-form-index-latest/latest-form-index.md', nextAction: '商务发客户前检查软件端去重表、排名连续性和风险提示。', acceptance: '固定表头、重复键为 0、同一 Brief 排名连续、中文可读。', boundary: '候选名单不等于最终投放名单,仍需商务复核和客户反馈。' }), row({ order: 6, section: '质量验收标准', status: roundPassed && evidenceReady ? 'passed' : 'open', evidence: `round.passed=${Boolean(ctx.round?.passed)},round.fail=${Number(ctx.round?.counts?.fail || 0)},evidence.total=${Number(ctx.evidence?.total || 0)}。`, currentArtifact: 'outputs/round-deposition-latest/round-deposition-summary.json', nextAction: '每轮结束继续运行 npm run round:refresh -- --output-root outputs --strict。', acceptance: 'round-deposition failCount=0;hygiene hitCount=0;acceptance 通过。', boundary: '验收通过只能说明当前工程链路可用,不自动证明客户选中率。' }), row({ order: 7, section: '给 AI 的优化任务', status: labelGuideReady ? 'passed' : 'open', evidence: `manualReviewLabelGuide.negativeLabelsRequireAttribution=${Boolean(ctx.latestForm?.manualReviewLabelGuide?.negativeLabelsRequireAttribution)},attributionTypeCount=${Number(ctx.latestForm?.manualReviewLabelGuide?.attributionTypeCount || 0)}。`, currentArtifact: 'outputs/manual-review-label-guide-latest/manual-review-label-guide.md', nextAction: '真实复核 CSV 到位后运行 review:metrics,并按主要失败原因单点优化。', acceptance: '负样本归因覆盖率=100%,并能沉淀为规则、权重或工作流变更。', boundary: '没有真实复核 CSV 时,只能验证标签体系,不能声明复核通过率提升。' }), row({ order: 8, section: '每轮沉淀', status: handoffReady && roundPassed ? 'passed' : 'open', evidence: `handoff.complete=${Boolean(ctx.handoff?.complete)},externalBlockers=${Number(ctx.handoff?.externalBlockers?.length || 0)},round.passCount=${Number(ctx.round?.counts?.pass || 0)}。`, currentArtifact: 'outputs/optimization-handoff-latest/optimization-handoff-report.md', nextAction: '继续更新 implementation log,并保持最新表单、交接摘要、证据台账同步。', acceptance: '实现日志、证据台账、状态报告、handoff 和 round-deposition 均刷新。', boundary: 'handoff.complete=false 是正确状态,说明还没有误报完成。' }) ]; } function row(value) { return { order: value.order, section: value.section, status: value.status, evidence: value.evidence, currentArtifact: value.currentArtifact, nextAction: value.nextAction, acceptance: value.acceptance, boundary: value.boundary }; } function normalizeOperatorPackOwnerArtifacts(ownerArtifacts) { if (!Array.isArray(ownerArtifacts)) return []; return ownerArtifacts.map(item => ({ owner: item.owner || '', actionCount: Number(item.actionCount || 0), topPriority: Number(item.topPriority || 0), markdown: String(item.markdown || '').replace(/\\/g, '/'), csv: String(item.csv || '').replace(/\\/g, '/'), gapIds: Array.isArray(item.gapIds) ? item.gapIds : [], evidenceRowCount: Number(item.evidenceRowCount || 0), nextActionCount: Number(item.nextActionCount || 0) })); } function renderReport(summary) { return [ '# 提号长期优化计划执行状态', '', `- 生成时间:${summary.generatedAt}`, `- 计划文件:${summary.plan}`, `- 是否完成:${summary.complete ? '是' : '否'}`, `- 证明缺口 openCount:${summary.proofOpenCount}`, `- proof-gap 操作包负责人附件:${summary.operatorPackOwnerArtifactCount || 0}`, `- 商务执行总表操作包负责人附件行:${summary.businessExecutionOperatorPackOwnerArtifactRowCount || 0}`, `- 状态计数:${Object.entries(summary.statusCounts).map(([key, value]) => `${key}=${value}`).join(',') || '无'}`, `- 证据计数:${Object.entries(summary.evidenceCounts).map(([key, value]) => `${key}=${value}`).join(',') || '无'}`, '', '## 执行总表', '', '| 顺序 | 计划章节 | 状态 | 当前证据 | 当前产物 | 下一步 | 验收标准 | 边界 |', '| ---: | --- | --- | --- | --- | --- | --- | --- |', ...summary.rows.map(item => tableRow([ item.order, item.section, item.status, item.evidence, item.currentArtifact, item.nextAction, item.acceptance, item.boundary ])), '', '## proof-gap 操作包负责人附件', '', '| 负责人 | 动作数 | Markdown | CSV | 缺口 |', '| --- | ---: | --- | --- | --- |', ...summary.operatorPackOwnerArtifacts.map(item => tableRow([ item.owner, item.actionCount, item.markdown || 'missing', item.csv || 'missing', (item.gapIds || []).join(';') || 'missing' ])), '', '## 防误报边界', '', ...summary.guardrails.map(item => `- ${item}`) ].join('\n'); } function renderCsv(rows) { const headers = ['顺序', '计划章节', '状态', '当前证据', '当前产物', '下一步', '验收标准', '边界']; return [ headers.join(','), ...rows.map(item => [ item.order, item.section, item.status, item.evidence, item.currentArtifact, item.nextAction, item.acceptance, item.boundary ].map(csvCell).join(',')) ].join('\n'); } function countStatuses(rows) { return rows.reduce((acc, row) => { acc[row.status] = (acc[row.status] || 0) + 1; return acc; }, {}); } function hasAll(text, phrases) { return phrases.every(phrase => text.includes(phrase)); } 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 readText(file) { if (!fs.existsSync(file)) return ''; return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''); } function rel(root, file) { return path.relative(root, file).replace(/\\/g, '/'); } function tableRow(cells) { return `| ${cells.map(escapeCell).join(' | ')} |`; } function escapeCell(value) { return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' '); } 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 = { buildPlanExecutionStatus, renderReport };