business-proof-progress.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const OUTPUTS = path.join(ROOT, 'outputs');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputDir = path.resolve(args.output || process.env.TIHAO_BUSINESS_PROOF_PROGRESS_OUTPUT || path.join(OUTPUTS, `business-proof-progress-${Date.now()}`));
  9. const summary = buildBusinessProofProgress({
  10. root: ROOT,
  11. outputsDir: path.resolve(args.outputs || OUTPUTS)
  12. });
  13. fs.mkdirSync(outputDir, { recursive: true });
  14. const jsonPath = path.join(outputDir, 'business-proof-progress-summary.json');
  15. const reportPath = path.join(outputDir, 'business-proof-progress-report.md');
  16. fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
  17. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  18. console.log(JSON.stringify({
  19. outputDir,
  20. json: jsonPath,
  21. report: reportPath,
  22. complete: summary.complete,
  23. pass: summary.counts.pass,
  24. blocked: summary.counts.blocked_by_external_data,
  25. pending: summary.counts.pending,
  26. fail: summary.counts.fail
  27. }, null, 2));
  28. if (!summary.complete && args.strict) process.exitCode = 1;
  29. }
  30. function buildBusinessProofProgress({ root, outputsDir }) {
  31. const dataPackDir = latestDir(outputsDir, 'data-intake-pack');
  32. const videoPackDir = latestDir(outputsDir, 'video-intake-pack');
  33. const intake = readJson(latestFile(outputsDir, 'intake-readiness-summary.json'));
  34. const videoReadiness = readJson(latestFile(outputsDir, 'video-resource-readiness-summary.json'));
  35. const longrun = readJson(latestFile(outputsDir, 'long-run-readiness-summary.json'));
  36. const closure = readJson(latestFile(outputsDir, 'proof-gap-closure-summary.json'));
  37. const handoff = readJson(latestFile(outputsDir, 'optimization-handoff-summary.json'));
  38. const evidence = readJson(latestFile(outputsDir, 'evidence-index-summary.json'));
  39. const round = readJson(latestFile(outputsDir, 'round-deposition-summary.json'));
  40. const status = readJson(latestFile(outputsDir, 'optimization-status-summary.json'));
  41. const history = readJson(latestFile(outputsDir, 'historical-dataset-audit.json'));
  42. const review = readJson(latestFile(outputsDir, 'review-metrics-summary.json'));
  43. const customerEffect = readJson(latestFile(outputsDir, 'customer-effect-summary.json'));
  44. const pipeline = readJson(latestFile(outputsDir, 'optimization-pipeline-summary.json'));
  45. const proofGapOperatorPack = readJson(latestFile(outputsDir, 'proof-gap-operator-pack-summary.json'));
  46. const businessExecutionIndex = readJson(latestFile(outputsDir, 'business-execution-index-summary.json'));
  47. const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts(
  48. proofGapOperatorPack?.ownerArtifacts || businessExecutionIndex?.operatorPackOwnerArtifacts || []
  49. );
  50. const operatorPackOwnerArtifactCount = Number(proofGapOperatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0);
  51. const closureRow = (id) => Array.isArray(closure?.rows) ? closure.rows.find(row => row.id === id) : null;
  52. const intakeOverallReady = intake?.acceptance?.overallReady === true && Number(intake?.failureCount || 0) === 0;
  53. const videoReady = videoReadiness?.acceptance?.readyForVideoAbPreflight === true && Number(videoReadiness?.failureCount || 0) === 0;
  54. const historyMissingRequirements = getHistoryCustomerEffectProofMissingRequirements(history);
  55. const historyReady = historyMissingRequirements.length === 0;
  56. const reviewReady = review?.acceptance?.overallPass === true;
  57. const customerEffectReady = customerEffect?.acceptance?.overallPass === true;
  58. const videoAbStatus = statusItem(status, 'video-ab-proof');
  59. const videoAbPassed = videoAbStatus?.status === 'passed' && closureRow('video-ab-live-proof')?.status === 'closed';
  60. const videoAbEvidence = videoAbStatus
  61. ? `status=${videoAbStatus.status}, closure=${closureRow('video-ab-live-proof')?.status || 'missing'}, evidence=${videoAbStatus.evidence || ''}`
  62. : (closureRow('video-ab-live-proof')?.evidence || 'missing video-ab status row');
  63. const rows = [
  64. step(1, '商务', '生成统一收集包', dataPackDir ? 'pass' : 'pending', dataPackDir ? rel(root, dataPackDir) : 'missing data-intake-pack', '没有统一模板时先运行 data:intake-template。'),
  65. step(2, '商务/投放', '生成视频资源收集包', videoPackDir ? 'pass' : 'pending', videoPackDir ? rel(root, videoPackDir) : 'missing video-intake-pack', '没有视频模板时先运行 video:intake-template。'),
  66. step(3, '商务', '填真实历史 Brief', intake?.acceptance?.historyReady ? 'pass' : 'blocked_by_external_data', intake ? `historyReady=${Boolean(intake.acceptance?.historyReady)}, issues=${intake.history?.issues?.length || 0}` : 'missing intake-readiness', '补 5-10 个真实 Brief、人工名单、客户选择、拒绝原因和历史人工补号量基线。'),
  67. step(4, '商务/投放', '填真实视频资源', intake?.acceptance?.videoReady || videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `realCandidate=${videoReadiness.counts?.realCandidateRows || 0}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补真实参考视频和真实候选视频 URL、封面、ASR、帧图或正文证据。'),
  68. step(5, '技术/AI', '真实材料预审', intakeOverallReady ? 'pass' : 'fail', intake ? `overallReady=${Boolean(intake.acceptance?.overallReady)}, failureCount=${intake.failureCount || 0}` : 'missing intake-readiness', '先修 intake readiness 失败项,不能跳过预审。'),
  69. step(6, '技术/AI', '历史数据导入', history ? 'pass' : 'pending', history ? 'historical-dataset-audit.json present' : 'missing historical-dataset-audit.json', '真实历史 CSV 到位后运行 history:from-csv。'),
  70. step(7, '技术/AI', '历史数据审计', historyReady ? 'pass' : 'blocked_by_external_data', history ? `readyForCustomerEffectProof=${Boolean(history.acceptance?.readyForCustomerEffectProof)}, missing=${historyMissingRequirements.length}` : 'missing historical-dataset-audit.json', '运行 history:audit --strict,直到可支撑客户效果证明。'),
  71. step(8, '技术/AI', '视频资源就绪审计', videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `readyForVideoAbPreflight=${Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight)}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补齐真实候选视频后重跑 video:resource-readiness --strict。'),
  72. step(9, '技术/AI', '长跑前门禁', longrun?.ready ? 'pass' : 'fail', longrun ? `ready=${Boolean(longrun.ready)}, fail=${longrun.counts?.fail || 0}` : 'missing long-run-readiness', 'longrun:readiness ready=true 前不要启动 overnight。'),
  73. step(10, '技术/AI', '运行策略矩阵', pipeline?.readyForClaim ? 'pass' : 'pending', pipeline ? `readyForClaim=${Boolean(pipeline.readyForClaim)}, pass=${pipeline.counts?.pass || 0}, fail=${pipeline.counts?.fail || 0}` : 'missing optimization-pipeline-summary.json', '真实材料和长跑门禁通过后再跑 optimization:pipeline。'),
  74. step(11, '技术/AI', '视频 A/B 验收', videoAbPassed ? 'pass' : 'pending', videoAbEvidence, '只有真实视频 A/B 通过且 status 审计为 passed 时才可用于视频提升结论。'),
  75. step(12, '商务', '人工复核标注', intake?.acceptance?.reviewMetricsReady ? 'pass' : 'blocked_by_external_data', intake ? `reviewMetricsReady=${Boolean(intake.acceptance?.reviewMetricsReady)}, customerEffectReady=${Boolean(intake.acceptance?.customerEffectReady)}` : 'missing intake-readiness', '补商务复核标签、负样本归因、客户选择和本轮人工补号量。'),
  76. step(13, '技术/AI', '商务复核指标', reviewReady ? 'pass' : 'blocked_by_external_data', review ? `overallPass=${Boolean(review.acceptance?.overallPass)}` : 'missing review-metrics-summary.json', '已标注 CSV 到位后运行 review:metrics --strict。'),
  77. step(14, '技术/AI', '客户效果审计', customerEffectReady ? 'pass' : 'blocked_by_external_data', customerEffect ? `overallPass=${Boolean(customerEffect.acceptance?.overallPass)}` : 'missing customer-effect-summary.json', '客户选择、历史基线和本轮人工补号量到位后运行 customer-effect:audit --strict。'),
  78. step(15, '技术/AI', '刷新证据和交接', round?.passed && handoff && evidence ? 'pass' : 'pending', `round=${Boolean(round?.passed)}, handoff=${Boolean(handoff)}, evidence=${Boolean(evidence)}, operatorPackOwnerArtifactCount=${operatorPackOwnerArtifactCount}`, '每轮结束刷新 evidence:index、proof-gap:closure、handoff:summary 和 round:deposition。')
  79. ];
  80. const counts = countRows(rows);
  81. return {
  82. generatedAt: new Date().toISOString(),
  83. root,
  84. outputsDir,
  85. complete: rows.every(row => row.status === 'pass'),
  86. counts,
  87. observed: {
  88. intakeReady: intakeOverallReady,
  89. intakeFailureCount: intake ? Number(intake.failureCount || 0) : null,
  90. videoReady,
  91. videoFailureCount: videoReadiness ? Number(videoReadiness.failureCount || 0) : null,
  92. longrunReady: Boolean(longrun?.ready),
  93. longrunFailCount: longrun ? Number(longrun.counts?.fail || 0) : null,
  94. proofGapOpenCount: closure ? Number(closure.openCount || 0) : null,
  95. handoffComplete: Boolean(handoff?.complete),
  96. videoAbProofStatus: videoAbStatus?.status || null,
  97. proofGapOperatorPackOwnerArtifactCount: operatorPackOwnerArtifactCount,
  98. businessExecutionOperatorPackOwnerArtifactCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactCount || 0),
  99. businessExecutionOperatorPackOwnerArtifactRowCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0),
  100. evidenceCounts: evidence?.counts || {}
  101. },
  102. operatorPackOwnerArtifactCount,
  103. operatorPackOwnerArtifacts,
  104. guardrails: [
  105. '本进度表只同步补证执行状态,不能把 sample、smoke、模板、provider fallback 或接口 200 计为客户效果证明。',
  106. 'blocked_by_external_data 表示需要真实历史 Brief、真实视频资源、商务复核或客户选择数据。',
  107. '只有全部 15 步为 pass,且 customer-effect:audit 与 proof-gap:closure 通过后,才可进入长期完成判断。',
  108. 'proof-gap by-owner attachments are dispatch files only; they do not prove customer effect.',
  109. '不得在任何输入、报告或日志中写入 sessionToken、Authorization、模型 token 或 npm token。'
  110. ],
  111. rows
  112. };
  113. }
  114. function statusItem(status, id) {
  115. return Array.isArray(status?.items) ? status.items.find(item => item.id === id) : null;
  116. }
  117. function getHistoryCustomerEffectProofMissingRequirements(history) {
  118. if (!history) return ['missing historical-dataset-audit.json'];
  119. const missing = [];
  120. const acceptance = history.acceptance || {};
  121. if (acceptance.readyForCustomerEffectProof !== true) missing.push('acceptance.readyForCustomerEffectProof=true');
  122. if (Number(history.briefCount || 0) < 5) missing.push('briefCount>=5');
  123. if (acceptance.parseOk !== true) missing.push('acceptance.parseOk=true');
  124. if (acceptance.minBriefsMet !== true) missing.push('acceptance.minBriefsMet=true');
  125. if (acceptance.allHaveBriefText !== true) missing.push('acceptance.allHaveBriefText=true');
  126. if (acceptance.allHaveManualFinalList !== true) missing.push('acceptance.allHaveManualFinalList=true');
  127. if (acceptance.allHaveCustomerDecision !== true) missing.push('acceptance.allHaveCustomerDecision=true');
  128. if (acceptance.allHaveFeedbackReason !== true) missing.push('acceptance.allHaveFeedbackReason=true');
  129. if (acceptance.allHaveManualSupplementBaseline !== true) missing.push('acceptance.allHaveManualSupplementBaseline=true');
  130. if (acceptance.categoryCoverageMet !== true) missing.push('acceptance.categoryCoverageMet=true');
  131. if (Number(history.missingCriticalCount || 0) !== 0) missing.push('missingCriticalCount=0');
  132. if (!Array.isArray(history.items) || history.items.length < 5) missing.push('items.length>=5');
  133. if (Number(history.withManualFinalList || 0) < 5) missing.push('withManualFinalList>=5');
  134. if (Number(history.withCustomerDecision || 0) < 5) missing.push('withCustomerDecision>=5');
  135. if (Number(history.withRejectionReason || 0) < 1) missing.push('withRejectionReason>=1');
  136. if (Number(history.withManualSupplementBaseline || 0) < 5) missing.push('withManualSupplementBaseline>=5');
  137. return uniqueStrings(missing);
  138. }
  139. function step(order, owner, title, status, evidence, next) {
  140. return { order, owner, title, status, evidence, next };
  141. }
  142. function normalizeOperatorPackOwnerArtifacts(ownerArtifacts) {
  143. if (!Array.isArray(ownerArtifacts)) return [];
  144. return ownerArtifacts.map(item => ({
  145. owner: item.owner || '',
  146. actionCount: Number(item.actionCount || 0),
  147. topPriority: Number(item.topPriority || 0),
  148. markdown: String(item.markdown || '').replace(/\\/g, '/'),
  149. csv: String(item.csv || '').replace(/\\/g, '/'),
  150. gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
  151. evidenceRowCount: Number(item.evidenceRowCount || 0),
  152. nextActionCount: Number(item.nextActionCount || 0)
  153. }));
  154. }
  155. function renderReport(summary) {
  156. const lines = [
  157. '# 提号补证 15 步进度表',
  158. '',
  159. `- 生成时间:${summary.generatedAt}`,
  160. `- 是否完成:${summary.complete ? '是' : '否'}`,
  161. `- pass:${summary.counts.pass || 0}`,
  162. `- fail:${summary.counts.fail || 0}`,
  163. `- pending:${summary.counts.pending || 0}`,
  164. `- blocked_by_external_data:${summary.counts.blocked_by_external_data || 0}`,
  165. '',
  166. '## 当前观测',
  167. '',
  168. `- intakeReady:${summary.observed.intakeReady ? 'true' : 'false'}`,
  169. `- intakeFailureCount:${summary.observed.intakeFailureCount ?? 'unknown'}`,
  170. `- videoReady:${summary.observed.videoReady ? 'true' : 'false'}`,
  171. `- videoFailureCount:${summary.observed.videoFailureCount ?? 'unknown'}`,
  172. `- longrunReady:${summary.observed.longrunReady ? 'true' : 'false'}`,
  173. `- longrunFailCount:${summary.observed.longrunFailCount ?? 'unknown'}`,
  174. `- proofGapOpenCount:${summary.observed.proofGapOpenCount ?? 'unknown'}`,
  175. `- handoffComplete:${summary.observed.handoffComplete ? 'true' : 'false'}`,
  176. `- videoAbProofStatus:${summary.observed.videoAbProofStatus ?? 'unknown'}`,
  177. `- proofGapOperatorPackOwnerArtifactCount:${summary.observed.proofGapOperatorPackOwnerArtifactCount ?? 'unknown'}`,
  178. `- businessExecutionOperatorPackOwnerArtifactCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactCount ?? 'unknown'}`,
  179. `- businessExecutionOperatorPackOwnerArtifactRowCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactRowCount ?? 'unknown'}`,
  180. '',
  181. '## 15 步进度',
  182. '',
  183. '| 顺序 | 负责人 | 目标 | 状态 | 证据 | 下一步 |',
  184. '| --- | --- | --- | --- | --- | --- |',
  185. ...summary.rows.map(row => `| ${row.order} | ${escapeCell(row.owner)} | ${escapeCell(row.title)} | ${row.status} | ${escapeCell(row.evidence)} | ${escapeCell(row.next)} |`),
  186. '',
  187. '## proof-gap 操作包负责人附件',
  188. '',
  189. '| 负责人 | 动作数 | Markdown | CSV | 缺口 |',
  190. '| --- | ---: | --- | --- | --- |',
  191. ...summary.operatorPackOwnerArtifacts.map(item => `| ${escapeCell(item.owner)} | ${item.actionCount} | ${escapeCell(item.markdown)} | ${escapeCell(item.csv)} | ${escapeCell((item.gapIds || []).join(';'))} |`),
  192. '',
  193. '## 边界',
  194. '',
  195. ...summary.guardrails.map(item => `- ${item}`)
  196. ];
  197. return lines.join('\n');
  198. }
  199. function latestDir(outputsDir, prefix) {
  200. if (!fs.existsSync(outputsDir)) return '';
  201. const dirs = fs.readdirSync(outputsDir, { withFileTypes: true })
  202. .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
  203. .map(entry => path.join(outputsDir, entry.name))
  204. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
  205. return dirs[0] || '';
  206. }
  207. function latestFile(outputsDir, fileName) {
  208. if (!fs.existsSync(outputsDir)) return '';
  209. return fs.readdirSync(outputsDir, { withFileTypes: true })
  210. .filter(entry => entry.isDirectory())
  211. .map(entry => path.join(outputsDir, entry.name, fileName))
  212. .filter(fileExists)
  213. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || '';
  214. }
  215. function readJson(file) {
  216. if (!file || !fs.existsSync(file)) return null;
  217. try {
  218. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  219. } catch {
  220. return null;
  221. }
  222. }
  223. function fileExists(file) {
  224. return Boolean(file) && fs.existsSync(file);
  225. }
  226. function countRows(rows) {
  227. return rows.reduce((acc, row) => {
  228. acc[row.status] = (acc[row.status] || 0) + 1;
  229. return acc;
  230. }, {});
  231. }
  232. function uniqueStrings(values) {
  233. return [...new Set((values || []).filter(Boolean).map(value => String(value)))];
  234. }
  235. function parseArgs(argv) {
  236. const args = {};
  237. for (let index = 0; index < argv.length; index += 1) {
  238. const raw = argv[index];
  239. if (!raw.startsWith('--')) continue;
  240. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  241. const next = argv[index + 1];
  242. if (!next || next.startsWith('--')) args[key] = true;
  243. else {
  244. args[key] = next;
  245. index += 1;
  246. }
  247. }
  248. return args;
  249. }
  250. function rel(root, file) {
  251. return path.relative(root, file).replace(/\\/g, '/');
  252. }
  253. function escapeCell(value) {
  254. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  255. }
  256. function withBom(text) {
  257. return `\uFEFF${text}`;
  258. }
  259. if (require.main === module) main();
  260. module.exports = {
  261. buildBusinessProofProgress,
  262. renderReport
  263. };