| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- #!/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
- };
|