plan-execution-status.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'plan-execution-status-latest');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs'));
  9. const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
  10. const summary = buildPlanExecutionStatus({ root: ROOT, outputsDir });
  11. fs.mkdirSync(outputDir, { recursive: true });
  12. const summaryPath = path.join(outputDir, 'plan-execution-status-summary.json');
  13. const reportPath = path.join(outputDir, 'plan-execution-status.md');
  14. const csvPath = path.join(outputDir, 'plan-execution-status.csv');
  15. fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
  16. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  17. fs.writeFileSync(csvPath, withBom(renderCsv(summary.rows)), 'utf8');
  18. console.log(JSON.stringify({
  19. outputDir,
  20. summary: summaryPath,
  21. report: reportPath,
  22. csv: csvPath,
  23. complete: summary.complete,
  24. rowCount: summary.rowCount,
  25. proofOpenCount: summary.proofOpenCount
  26. }, null, 2));
  27. if (args.strict && !summary.passed) process.exitCode = 1;
  28. }
  29. function buildPlanExecutionStatus({ root, outputsDir }) {
  30. const planPath = path.join(root, 'docs', 'tihao-experience-optimization-plan.md');
  31. const planText = readText(planPath);
  32. const status = readJson(path.join(pickDir(outputsDir, 'optimization-status-latest', /^optimization-status-/), 'optimization-status-summary.json'));
  33. const closure = readJson(path.join(pickDir(outputsDir, 'proof-gap-closure-latest', /^proof-gap-closure-/), 'proof-gap-closure-summary.json'));
  34. const progress = readJson(path.join(pickDir(outputsDir, 'business-proof-progress-latest', /^business-proof-progress-/), 'business-proof-progress-summary.json'));
  35. const nextActions = readJson(path.join(pickDir(outputsDir, 'business-proof-next-actions-latest', /^business-proof-next-actions-/), 'business-proof-next-actions-summary.json'));
  36. const latestForm = readJson(path.join(pickDir(outputsDir, 'latest-form-index-latest', /^latest-form-index-/), 'latest-form-index-summary.json'));
  37. const handoff = readJson(path.join(pickDir(outputsDir, 'optimization-handoff-latest', /^optimization-handoff-/), 'optimization-handoff-summary.json'));
  38. const round = readJson(path.join(pickDir(outputsDir, 'round-deposition-latest', /^round-deposition-/), 'round-deposition-summary.json'));
  39. const evidence = readJson(path.join(pickDir(outputsDir, 'evidence-index-latest', /^evidence-index-/), 'evidence-index-summary.json'));
  40. const proofGapOperatorPack = readJson(path.join(pickDir(outputsDir, 'proof-gap-operator-pack-latest', /^proof-gap-operator-pack-/), 'proof-gap-operator-pack-summary.json'));
  41. const businessExecutionIndex = readJson(path.join(pickDir(outputsDir, 'business-execution-index-latest', /^business-execution-index-/), 'business-execution-index-summary.json'));
  42. const proofOpenCount = Number(closure?.openCount ?? latestForm?.nextActions?.proofOpenCount ?? 0);
  43. const statusCounts = status?.counts || {};
  44. const progressCounts = progress?.counts || {};
  45. const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts(
  46. businessExecutionIndex?.operatorPackOwnerArtifacts || proofGapOperatorPack?.ownerArtifacts || []
  47. );
  48. const operatorPackOwnerArtifactCount = Number(proofGapOperatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0);
  49. const rows = buildRows({
  50. planText,
  51. status,
  52. closure,
  53. progress,
  54. nextActions,
  55. latestForm,
  56. handoff,
  57. round,
  58. evidence,
  59. proofGapOperatorPack,
  60. businessExecutionIndex,
  61. operatorPackOwnerArtifactCount,
  62. proofOpenCount
  63. });
  64. const counts = countStatuses(rows);
  65. return {
  66. generatedAt: new Date().toISOString(),
  67. root,
  68. outputsDir,
  69. plan: rel(root, planPath),
  70. passed: rows.every(row => row.status !== 'missing'),
  71. complete: proofOpenCount === 0 && Boolean(closure?.complete) && Boolean(status?.complete),
  72. rowCount: rows.length,
  73. proofOpenCount,
  74. statusCounts,
  75. progressCounts,
  76. roundPassed: Boolean(round?.passed),
  77. roundPassCount: Number(round?.counts?.pass || 0),
  78. roundFailCount: Number(round?.counts?.fail || 0),
  79. evidenceCounts: evidence?.counts || {},
  80. nextActionCount: Array.isArray(nextActions?.actions) ? nextActions.actions.length : 0,
  81. topNextAction: nextActions?.actions?.[0]?.title || '',
  82. operatorPackOwnerArtifactCount,
  83. operatorPackOwnerArtifacts,
  84. businessExecutionOperatorPackOwnerArtifactCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactCount || 0),
  85. businessExecutionOperatorPackOwnerArtifactRowCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0),
  86. files: {
  87. summary: 'outputs/plan-execution-status-latest/plan-execution-status-summary.json',
  88. report: 'outputs/plan-execution-status-latest/plan-execution-status.md',
  89. csv: 'outputs/plan-execution-status-latest/plan-execution-status.csv'
  90. },
  91. guardrails: [
  92. '本面板只同步计划执行进度,不证明客户效果。',
  93. 'proofOpenCount 不为 0 时,不得宣称命中率提升、客户效果达标或人工补号量下降。',
  94. 'sample、smoke、provider fallback、接口 200 只能证明流程或接口可用,不能证明业务效果。',
  95. '不得在报告、表格、日志或命令中写入 sessionToken、Authorization、模型 token 或 npm token。'
  96. ],
  97. rows
  98. };
  99. }
  100. function buildRows(ctx) {
  101. const planReadable = hasAll(ctx.planText, ['核心判断', '关键差距', '质量验收标准', '每轮沉淀']);
  102. const releasePassed = Number(ctx.status?.counts?.passed || 0) >= 20;
  103. const proofOpen = Number(ctx.proofOpenCount || 0);
  104. const nextActionCount = Array.isArray(ctx.nextActions?.actions) ? ctx.nextActions.actions.length : 0;
  105. const latestFormReady = Boolean(ctx.latestForm?.passed);
  106. const roundPassed = Boolean(ctx.round?.passed) && Number(ctx.round?.counts?.fail || 0) === 0;
  107. const labelGuideReady = Boolean(ctx.latestForm?.manualReviewLabelGuide?.negativeLabelsRequireAttribution);
  108. const evidenceReady = Number(ctx.evidence?.total || 0) > 0;
  109. const handoffReady = ctx.handoff && ctx.handoff.complete === false && Number(ctx.handoff.externalBlockers?.length || 0) >= 1;
  110. return [
  111. row({
  112. order: 1,
  113. section: '核心判断',
  114. status: planReadable ? 'passed' : 'missing',
  115. evidence: planReadable ? '主计划已是中文可读,并保留核心判断、差距、验收和每轮沉淀。' : '主计划缺少关键中文段落。',
  116. currentArtifact: 'docs/tihao-experience-optimization-plan.md',
  117. nextAction: '持续把真实业务反馈转成规则、权重或工作流变更。',
  118. acceptance: '计划文件中文可读;不含乱码;不宣称未证明效果。',
  119. boundary: '计划可读不等于业务效果已证明。'
  120. }),
  121. row({
  122. order: 2,
  123. section: '当前已具备能力',
  124. status: releasePassed ? 'passed' : 'missing',
  125. 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)}。`,
  126. currentArtifact: 'outputs/optimization-status-latest/optimization-status-summary.json',
  127. nextAction: '发布或交付前继续运行 npm run acceptance。',
  128. acceptance: '基础链路、软件表、硬性规则、反馈闭环、证据台账等能力有脚本或 smoke 保护。',
  129. boundary: '能力就绪项多为 smoke/local 证明,不能替代客户效果审计。'
  130. }),
  131. row({
  132. order: 3,
  133. section: '关键差距',
  134. status: proofOpen > 0 ? 'open' : 'passed',
  135. evidence: `proof-gap-closure.openCount=${proofOpen},topNextAction=${ctx.nextActions?.actions?.[0]?.title || '无'},operatorPackOwnerArtifactCount=${Number(ctx.operatorPackOwnerArtifactCount || 0)}。`,
  136. currentArtifact: 'outputs/proof-gap-closure-latest/proof-gap-closure-summary.json',
  137. nextAction: ctx.nextActions?.actions?.[0]?.command || '按 proof-gap-closure 的 open rows 补齐真实证据。',
  138. acceptance: 'openCount=0 且 status.complete=true 后,才进入完成判断。',
  139. boundary: 'openCount>0 时不得宣称长期优化目标完成。'
  140. }),
  141. row({
  142. order: 4,
  143. section: '长跑优化任务',
  144. status: nextActionCount > 0 ? 'open' : 'passed',
  145. evidence: `business-proof-next-actions.actionCount=${nextActionCount};business-proof-progress.complete=${Boolean(ctx.progress?.complete)},businessExecutionOperatorPackOwnerArtifactRowCount=${Number(ctx.businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0)}。`,
  146. currentArtifact: 'outputs/business-proof-next-actions-latest/business-proof-next-actions-report.md',
  147. nextAction: ctx.nextActions?.actions?.[0]?.title ? `优先处理:${ctx.nextActions.actions[0].title}` : '继续运行策略矩阵并沉淀结果。',
  148. acceptance: '真实历史数据、视频资源、复核指标和客户效果审计形成闭环。',
  149. boundary: '当前行动队列是补证任务,不是效果证明。'
  150. }),
  151. row({
  152. order: 5,
  153. section: '软件端表格验收',
  154. status: latestFormReady ? 'passed' : 'open',
  155. evidence: `latest-form-index.passed=${Boolean(ctx.latestForm?.passed)},primaryForm=${ctx.latestForm?.primaryForm?.csv || 'missing'},clientList=${ctx.latestForm?.clientList?.csv || 'missing'}。`,
  156. currentArtifact: 'outputs/latest-form-index-latest/latest-form-index.md',
  157. nextAction: '商务发客户前检查软件端去重表、排名连续性和风险提示。',
  158. acceptance: '固定表头、重复键为 0、同一 Brief 排名连续、中文可读。',
  159. boundary: '候选名单不等于最终投放名单,仍需商务复核和客户反馈。'
  160. }),
  161. row({
  162. order: 6,
  163. section: '质量验收标准',
  164. status: roundPassed && evidenceReady ? 'passed' : 'open',
  165. evidence: `round.passed=${Boolean(ctx.round?.passed)},round.fail=${Number(ctx.round?.counts?.fail || 0)},evidence.total=${Number(ctx.evidence?.total || 0)}。`,
  166. currentArtifact: 'outputs/round-deposition-latest/round-deposition-summary.json',
  167. nextAction: '每轮结束继续运行 npm run round:refresh -- --output-root outputs --strict。',
  168. acceptance: 'round-deposition failCount=0;hygiene hitCount=0;acceptance 通过。',
  169. boundary: '验收通过只能说明当前工程链路可用,不自动证明客户选中率。'
  170. }),
  171. row({
  172. order: 7,
  173. section: '给 AI 的优化任务',
  174. status: labelGuideReady ? 'passed' : 'open',
  175. evidence: `manualReviewLabelGuide.negativeLabelsRequireAttribution=${Boolean(ctx.latestForm?.manualReviewLabelGuide?.negativeLabelsRequireAttribution)},attributionTypeCount=${Number(ctx.latestForm?.manualReviewLabelGuide?.attributionTypeCount || 0)}。`,
  176. currentArtifact: 'outputs/manual-review-label-guide-latest/manual-review-label-guide.md',
  177. nextAction: '真实复核 CSV 到位后运行 review:metrics,并按主要失败原因单点优化。',
  178. acceptance: '负样本归因覆盖率=100%,并能沉淀为规则、权重或工作流变更。',
  179. boundary: '没有真实复核 CSV 时,只能验证标签体系,不能声明复核通过率提升。'
  180. }),
  181. row({
  182. order: 8,
  183. section: '每轮沉淀',
  184. status: handoffReady && roundPassed ? 'passed' : 'open',
  185. evidence: `handoff.complete=${Boolean(ctx.handoff?.complete)},externalBlockers=${Number(ctx.handoff?.externalBlockers?.length || 0)},round.passCount=${Number(ctx.round?.counts?.pass || 0)}。`,
  186. currentArtifact: 'outputs/optimization-handoff-latest/optimization-handoff-report.md',
  187. nextAction: '继续更新 implementation log,并保持最新表单、交接摘要、证据台账同步。',
  188. acceptance: '实现日志、证据台账、状态报告、handoff 和 round-deposition 均刷新。',
  189. boundary: 'handoff.complete=false 是正确状态,说明还没有误报完成。'
  190. })
  191. ];
  192. }
  193. function row(value) {
  194. return {
  195. order: value.order,
  196. section: value.section,
  197. status: value.status,
  198. evidence: value.evidence,
  199. currentArtifact: value.currentArtifact,
  200. nextAction: value.nextAction,
  201. acceptance: value.acceptance,
  202. boundary: value.boundary
  203. };
  204. }
  205. function normalizeOperatorPackOwnerArtifacts(ownerArtifacts) {
  206. if (!Array.isArray(ownerArtifacts)) return [];
  207. return ownerArtifacts.map(item => ({
  208. owner: item.owner || '',
  209. actionCount: Number(item.actionCount || 0),
  210. topPriority: Number(item.topPriority || 0),
  211. markdown: String(item.markdown || '').replace(/\\/g, '/'),
  212. csv: String(item.csv || '').replace(/\\/g, '/'),
  213. gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
  214. evidenceRowCount: Number(item.evidenceRowCount || 0),
  215. nextActionCount: Number(item.nextActionCount || 0)
  216. }));
  217. }
  218. function renderReport(summary) {
  219. return [
  220. '# 提号长期优化计划执行状态',
  221. '',
  222. `- 生成时间:${summary.generatedAt}`,
  223. `- 计划文件:${summary.plan}`,
  224. `- 是否完成:${summary.complete ? '是' : '否'}`,
  225. `- 证明缺口 openCount:${summary.proofOpenCount}`,
  226. `- proof-gap 操作包负责人附件:${summary.operatorPackOwnerArtifactCount || 0}`,
  227. `- 商务执行总表操作包负责人附件行:${summary.businessExecutionOperatorPackOwnerArtifactRowCount || 0}`,
  228. `- 状态计数:${Object.entries(summary.statusCounts).map(([key, value]) => `${key}=${value}`).join(',') || '无'}`,
  229. `- 证据计数:${Object.entries(summary.evidenceCounts).map(([key, value]) => `${key}=${value}`).join(',') || '无'}`,
  230. '',
  231. '## 执行总表',
  232. '',
  233. '| 顺序 | 计划章节 | 状态 | 当前证据 | 当前产物 | 下一步 | 验收标准 | 边界 |',
  234. '| ---: | --- | --- | --- | --- | --- | --- | --- |',
  235. ...summary.rows.map(item => tableRow([
  236. item.order,
  237. item.section,
  238. item.status,
  239. item.evidence,
  240. item.currentArtifact,
  241. item.nextAction,
  242. item.acceptance,
  243. item.boundary
  244. ])),
  245. '',
  246. '## proof-gap 操作包负责人附件',
  247. '',
  248. '| 负责人 | 动作数 | Markdown | CSV | 缺口 |',
  249. '| --- | ---: | --- | --- | --- |',
  250. ...summary.operatorPackOwnerArtifacts.map(item => tableRow([
  251. item.owner,
  252. item.actionCount,
  253. item.markdown || 'missing',
  254. item.csv || 'missing',
  255. (item.gapIds || []).join(';') || 'missing'
  256. ])),
  257. '',
  258. '## 防误报边界',
  259. '',
  260. ...summary.guardrails.map(item => `- ${item}`)
  261. ].join('\n');
  262. }
  263. function renderCsv(rows) {
  264. const headers = ['顺序', '计划章节', '状态', '当前证据', '当前产物', '下一步', '验收标准', '边界'];
  265. return [
  266. headers.join(','),
  267. ...rows.map(item => [
  268. item.order,
  269. item.section,
  270. item.status,
  271. item.evidence,
  272. item.currentArtifact,
  273. item.nextAction,
  274. item.acceptance,
  275. item.boundary
  276. ].map(csvCell).join(','))
  277. ].join('\n');
  278. }
  279. function countStatuses(rows) {
  280. return rows.reduce((acc, row) => {
  281. acc[row.status] = (acc[row.status] || 0) + 1;
  282. return acc;
  283. }, {});
  284. }
  285. function hasAll(text, phrases) {
  286. return phrases.every(phrase => text.includes(phrase));
  287. }
  288. function pickDir(outputsDir, latestName, pattern) {
  289. const latest = path.join(outputsDir, latestName);
  290. if (fs.existsSync(latest)) return latest;
  291. if (!fs.existsSync(outputsDir)) return latest;
  292. const dirs = fs.readdirSync(outputsDir, { withFileTypes: true })
  293. .filter(entry => entry.isDirectory() && pattern.test(entry.name))
  294. .map(entry => path.join(outputsDir, entry.name))
  295. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
  296. return dirs[0] || latest;
  297. }
  298. function readJson(file) {
  299. if (!fs.existsSync(file)) return null;
  300. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  301. }
  302. function readText(file) {
  303. if (!fs.existsSync(file)) return '';
  304. return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
  305. }
  306. function rel(root, file) {
  307. return path.relative(root, file).replace(/\\/g, '/');
  308. }
  309. function tableRow(cells) {
  310. return `| ${cells.map(escapeCell).join(' | ')} |`;
  311. }
  312. function escapeCell(value) {
  313. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  314. }
  315. function csvCell(value) {
  316. const text = String(value ?? '');
  317. return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  318. }
  319. function parseArgs(argv) {
  320. const args = {};
  321. for (let index = 0; index < argv.length; index += 1) {
  322. const raw = argv[index];
  323. if (!raw.startsWith('--')) continue;
  324. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  325. const next = argv[index + 1];
  326. if (!next || next.startsWith('--')) args[key] = true;
  327. else {
  328. args[key] = next;
  329. index += 1;
  330. }
  331. }
  332. return args;
  333. }
  334. function withBom(text) {
  335. return `\uFEFF${text}`;
  336. }
  337. if (require.main === module) main();
  338. module.exports = {
  339. buildPlanExecutionStatus,
  340. renderReport
  341. };