real-proof-intake-bundle.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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. const DEFAULT_OUTPUT = path.join(OUTPUTS, 'real-proof-intake-bundle-latest');
  7. function main() {
  8. const args = parseArgs(process.argv.slice(2));
  9. const outputsDir = path.resolve(args.outputs || OUTPUTS);
  10. const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
  11. const summary = buildRealProofIntakeBundle({ root: ROOT, outputsDir });
  12. fs.mkdirSync(outputDir, { recursive: true });
  13. const summaryPath = path.join(outputDir, 'real-proof-intake-bundle-summary.json');
  14. const reportPath = path.join(outputDir, 'real-proof-intake-bundle.md');
  15. const csvPath = path.join(outputDir, 'real-proof-intake-bundle.csv');
  16. fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
  17. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  18. fs.writeFileSync(csvPath, withBom(renderCsv(summary.exportRows)), 'utf8');
  19. console.log(JSON.stringify({
  20. outputDir,
  21. summary: summaryPath,
  22. report: reportPath,
  23. csv: csvPath,
  24. passed: summary.passed,
  25. complete: summary.complete,
  26. proofOpenCount: summary.proofOpenCount,
  27. exportRowCount: summary.exportRowCount
  28. }, null, 2));
  29. if (args.strict && !summary.passed) process.exitCode = 1;
  30. }
  31. function buildRealProofIntakeBundle({ root, outputsDir }) {
  32. const dirs = {
  33. dataPack: pickDir(outputsDir, /^data-intake-pack-/) || path.join(outputsDir, 'data-intake-pack-latest'),
  34. videoPack: pickDir(outputsDir, /^video-intake-pack-/) || path.join(outputsDir, 'video-intake-pack-latest'),
  35. intakeChecklist: pickDir(outputsDir, /^intake-field-checklist-/) || path.join(outputsDir, 'intake-field-checklist-latest'),
  36. operatorPack: pickDir(outputsDir, /^proof-gap-operator-pack-/) || path.join(outputsDir, 'proof-gap-operator-pack-latest'),
  37. proofGapClosure: pickDir(outputsDir, /^proof-gap-closure-/) || path.join(outputsDir, 'proof-gap-closure-latest'),
  38. latestFormIndex: pickDir(outputsDir, /^latest-form-index-/) || path.join(outputsDir, 'latest-form-index-latest'),
  39. manualReviewGuide: pickDir(outputsDir, /^manual-review-label-guide-/) || path.join(outputsDir, 'manual-review-label-guide-latest'),
  40. businessExecution: pickDir(outputsDir, /^business-execution-index-/) || path.join(outputsDir, 'business-execution-index-latest'),
  41. intakeReadiness: pickDir(outputsDir, /^intake-readiness-/) || path.join(outputsDir, 'intake-readiness-latest'),
  42. videoReadiness: pickDir(outputsDir, /^video-resource-readiness-/) || path.join(outputsDir, 'video-resource-readiness-latest'),
  43. homepageReadiness: pickDir(outputsDir, /^homepage-evidence-readiness-/) || path.join(outputsDir, 'homepage-evidence-readiness-latest'),
  44. planStatus: pickDir(outputsDir, /^plan-execution-status-/) || path.join(outputsDir, 'plan-execution-status-latest'),
  45. optimizationCompletion: pickDir(outputsDir, /^optimization-completion-/) || path.join(outputsDir, 'optimization-completion-latest'),
  46. longRunReadiness: pickDir(outputsDir, /^long-run-readiness-/) || path.join(outputsDir, 'long-run-readiness-latest')
  47. };
  48. const summaries = {
  49. intakeChecklist: readJson(path.join(dirs.intakeChecklist, 'intake-field-checklist-summary.json')),
  50. operatorPack: readJson(path.join(dirs.operatorPack, 'proof-gap-operator-pack-summary.json')),
  51. proofGapClosure: readJson(path.join(dirs.proofGapClosure, 'proof-gap-closure-summary.json')),
  52. latestFormIndex: readJson(path.join(dirs.latestFormIndex, 'latest-form-index-summary.json')),
  53. manualReviewGuide: readJson(path.join(dirs.manualReviewGuide, 'manual-review-label-guide-summary.json')),
  54. businessExecution: readJson(path.join(dirs.businessExecution, 'business-execution-index-summary.json')),
  55. intakeReadiness: readJson(path.join(dirs.intakeReadiness, 'intake-readiness-summary.json')),
  56. videoReadiness: readJson(path.join(dirs.videoReadiness, 'video-resource-readiness-summary.json')),
  57. homepageReadiness: readJson(path.join(dirs.homepageReadiness, 'homepage-evidence-readiness-summary.json')),
  58. planStatus: readJson(path.join(dirs.planStatus, 'plan-execution-status-summary.json')),
  59. optimizationCompletion: readJson(path.join(dirs.optimizationCompletion, 'optimization-completion-summary.json')),
  60. longRunReadiness: readJson(path.join(dirs.longRunReadiness, 'long-run-readiness-summary.json'))
  61. };
  62. const templateFiles = buildTemplateFiles(root, dirs, summaries);
  63. const fillRows = buildFillRows(summaries);
  64. const proofGapRows = buildProofGapRows(summaries.operatorPack, summaries.proofGapClosure);
  65. const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts(root, dirs.operatorPack, summaries.operatorPack);
  66. const repairRows = buildRepairRows(root, dirs, summaries);
  67. const recheckCommands = buildRecheckCommands(summaries);
  68. const exportRows = [
  69. ...templateFiles.map((item, index) => exportRow('打开文件', index + 1, item.owner, item.title, item.path, item.status, item.usage, item.boundary)),
  70. ...operatorPackOwnerArtifacts.map((item, index) => exportRow(
  71. '负责人附件',
  72. index + 1,
  73. item.owner,
  74. 'proof-gap 操作包负责人附件',
  75. [item.markdown, item.csv].filter(Boolean).join('; '),
  76. `actionCount=${item.actionCount}`,
  77. `topPriority=${item.topPriority || 'unknown'};gapIds=${item.gapIds.join('; ') || 'missing'}`,
  78. '负责人附件只用于补证派工,不证明客户效果。',
  79. item.gapIds.length,
  80. `evidenceRowCount=${item.evidenceRowCount}`,
  81. `nextActionCount=${item.nextActionCount}`
  82. )),
  83. ...fillRows.map(item => exportRow('填表顺序', item.order, item.owner, item.title, item.file, item.status, item.acceptance, item.boundary, item.issueCount)),
  84. ...proofGapRows.map(item => exportRow('证明缺口', item.priority, item.owner, item.title, item.fillFiles.join('; '), item.status, item.acceptance, item.boundary, '', item.evidence, item.nextAction)),
  85. ...repairRows.map((item, index) => exportRow('修复清单', index + 1, item.owner, item.title, item.csv, item.status, item.acceptance, item.boundary, item.issueCount)),
  86. ...recheckCommands.map((item, index) => exportRow('复验命令', index + 1, item.owner, item.stage, item.command, '待真实材料补齐后运行', item.acceptance, item.boundary))
  87. ];
  88. const proofOpenCount = Number(summaries.proofGapClosure?.openCount ?? summaries.operatorPack?.openCount ?? summaries.planStatus?.proofOpenCount ?? 0);
  89. const operatorPackEvidenceRowCount = Number(summaries.operatorPack?.evidenceRowCount || countRowsWith(proofGapRows, row => row.evidence));
  90. const operatorPackNextActionCount = Number(summaries.operatorPack?.nextActionCount || countRowsWith(proofGapRows, row => row.nextAction));
  91. const operatorPackMissingProofRequirementCount = Number(summaries.operatorPack?.missingProofRequirementCount || proofGapRows.reduce((total, row) => total + row.missingProofRequirements.length, 0));
  92. const operatorPackOwnerArtifactCount = Number(summaries.operatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0);
  93. const expectedOperatorPackOwnerArtifactCount = expectedOwnerArtifactCount(summaries.operatorPack, operatorPackOwnerArtifacts);
  94. const complete = Boolean(
  95. summaries.proofGapClosure?.complete &&
  96. summaries.planStatus?.complete &&
  97. summaries.optimizationCompletion?.complete &&
  98. summaries.optimizationCompletion?.readyForClaim &&
  99. proofOpenCount === 0
  100. );
  101. const passed = templateFiles.some(item => item.path && item.path.includes('history-data-template.csv')) &&
  102. templateFiles.some(item => item.path && item.path.includes('manual-review-template.csv')) &&
  103. templateFiles.some(item => item.path && item.path.includes('video-resource-template.csv')) &&
  104. templateFiles.some(item => item.path && item.path.includes('long-run-readiness-report.md')) &&
  105. fillRows.length >= 3 &&
  106. proofGapRows.length >= 5 &&
  107. recheckCommands.some(item => item.command.includes('intake:readiness')) &&
  108. recheckCommands.some(item => item.command.includes('customer-effect:audit')) &&
  109. recheckCommands.some(item => item.command.includes('acceptance:video-ab')) &&
  110. recheckCommands.some(item => item.command.includes('longrun:readiness')) &&
  111. operatorPackEvidenceRowCount >= proofGapRows.length &&
  112. operatorPackNextActionCount >= proofGapRows.length &&
  113. operatorPackOwnerArtifactCount >= expectedOperatorPackOwnerArtifactCount &&
  114. operatorPackOwnerArtifacts.every(item => item.markdown && item.csv && item.evidenceRowCount >= 1 && item.nextActionCount >= 1) &&
  115. proofGapRows.every(item => item.evidence && item.nextAction) &&
  116. exportRows.every(item => /不.*证明|不得.*宣称|不能|只证明/.test(item.boundary || ''));
  117. return {
  118. generatedAt: new Date().toISOString(),
  119. root,
  120. outputsDir,
  121. passed,
  122. complete,
  123. directCustomerProof: false,
  124. proofLevel: 'not_business_proof',
  125. proofOpenCount,
  126. proofGapRowCount: proofGapRows.length,
  127. fillRowCount: fillRows.length,
  128. repairRowCount: repairRows.length,
  129. recheckCommandCount: recheckCommands.length,
  130. exportRowCount: exportRows.length,
  131. operatorPackEvidenceRowCount,
  132. operatorPackNextActionCount,
  133. operatorPackMissingProofRequirementCount,
  134. operatorPackOwnerArtifactCount,
  135. expectedOperatorPackOwnerArtifactCount,
  136. sourceState: {
  137. intakeChecklistPassed: Boolean(summaries.intakeChecklist?.passed),
  138. intakeFieldItemCount: Number(summaries.intakeChecklist?.itemCount || 0),
  139. intakeMissingRequiredCount: Number(summaries.intakeChecklist?.missingRequiredCount || 0),
  140. intakePlaceholderCount: Number(summaries.intakeChecklist?.placeholderCount || 0),
  141. operatorPackPassed: Boolean(summaries.operatorPack?.passed),
  142. operatorPackOpenCount: Number(summaries.operatorPack?.openCount || 0),
  143. operatorPackEvidenceRowCount,
  144. operatorPackNextActionCount,
  145. operatorPackMissingProofRequirementCount,
  146. operatorPackOwnerArtifactCount,
  147. expectedOperatorPackOwnerArtifactCount,
  148. latestFormPassed: Boolean(summaries.latestFormIndex?.passed),
  149. planExecutionComplete: Boolean(summaries.planStatus?.complete),
  150. optimizationCompletionComplete: Boolean(summaries.optimizationCompletion?.complete),
  151. optimizationCompletionReadyForClaim: Boolean(summaries.optimizationCompletion?.readyForClaim),
  152. longRunReadinessReady: Boolean(summaries.longRunReadiness?.ready),
  153. longRunReadinessFailCount: Number(summaries.longRunReadiness?.counts?.fail ?? summaries.longRunReadiness?.failCount ?? 0),
  154. longRunReadinessProofLevel: summaries.longRunReadiness ? 'not_business_proof' : '',
  155. longRunReadinessDirectCustomerProof: false,
  156. intakeReady: Boolean(summaries.intakeReadiness?.acceptance?.overallReady),
  157. videoReady: Boolean(summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight)
  158. },
  159. files: {
  160. summary: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle-summary.json',
  161. report: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle.md',
  162. csv: 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle.csv',
  163. operatorPackByOwnerDir: relIfExists(root, path.join(dirs.operatorPack, 'by-owner'))
  164. },
  165. templateFiles,
  166. operatorPackOwnerArtifacts,
  167. fillRows,
  168. proofGapRows,
  169. repairRows,
  170. recheckCommands,
  171. exportRows,
  172. guardrails: [
  173. '本总包只把真实材料入口、字段缺口、修复清单和复验命令集中到一个地方,不证明客户效果。',
  174. '必须把 example.com、示例、候选、待补、待客户反馈等占位内容替换成真实客户材料。',
  175. 'proofOpenCount 不为 0 时,不得宣称提号率提升、客户效果达标或人工补号量下降。',
  176. 'optimization-completion.readyForClaim=false 时,不得宣称长期优化完成。',
  177. 'longrun:readiness.ready=false 时,不能启动或宣称 full-matrix/live 长跑完成。',
  178. '缺真实视频 URL、封面、字幕、ASR、帧图或正文时,不能声明视频分析完成。',
  179. '不得在表格、报告、日志或命令中写入 sessionToken、鉴权头、模型 token 或 npm token。'
  180. ]
  181. };
  182. }
  183. function buildTemplateFiles(root, dirs, summaries) {
  184. return [
  185. {
  186. owner: '商务',
  187. title: '历史 Brief 数据模板',
  188. path: relIfExists(root, path.join(dirs.dataPack, 'history-data-template.csv')),
  189. status: summaries.intakeReadiness?.acceptance?.historyReady ? '已就绪' : '待填真实历史数据',
  190. usage: '填写真实历史 Brief、参考账号/视频、人工最终名单、客户选择和历史人工补号量基线。',
  191. boundary: '模板存在不证明客户效果。'
  192. },
  193. {
  194. owner: '商务',
  195. title: '人工复核模板',
  196. path: relIfExists(root, path.join(dirs.dataPack, 'manual-review-template.csv')),
  197. status: summaries.intakeReadiness?.acceptance?.reviewReady ? '已就绪' : '待填人工复核和客户选择',
  198. usage: '填写候选人工复核标签、客户选择、负样本归因、反馈原因和本轮人工补号量。',
  199. boundary: '复核模板存在不证明客户效果。'
  200. },
  201. {
  202. owner: '商务/投放',
  203. title: '视频资源模板',
  204. path: relIfExists(root, path.join(dirs.videoPack, 'video-resource-template.csv')),
  205. status: summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight ? '可进入视频 A/B 前置' : '待补真实视频资源',
  206. usage: '填写真实参考视频和真实候选视频 URL,并补封面、ASR、帧图、标题或内容摘要证据。',
  207. boundary: '视频资源模板存在不证明视频提升提号率。'
  208. },
  209. {
  210. owner: '商务/投放',
  211. title: '字段级补齐核对表',
  212. path: relIfExists(root, path.join(dirs.intakeChecklist, 'intake-field-checklist.md')),
  213. status: summaries.intakeChecklist?.passed ? `待处理 ${Number(summaries.intakeChecklist.itemCount || 0)} 项` : '缺失或未生成',
  214. usage: '按行号、字段、负责人和修复动作逐格补齐真实材料。',
  215. boundary: '字段核对表只安排补齐动作,不证明客户效果。'
  216. },
  217. {
  218. owner: '商务/技术/AI',
  219. title: '提号补证操作包',
  220. path: relIfExists(root, path.join(dirs.operatorPack, 'proof-gap-operator-pack.md')),
  221. status: summaries.operatorPack?.passed ? `openCount=${Number(summaries.operatorPack.openCount || 0)}` : '缺失或未生成',
  222. usage: `把五类真实证明缺口转成负责人、当前证据、下一步、填写文件、待补字段、复验命令和通过标准;当前 evidenceRowCount=${Number(summaries.operatorPack?.evidenceRowCount || 0)},nextActionCount=${Number(summaries.operatorPack?.nextActionCount || 0)}。`,
  223. boundary: '操作包只安排补证,不证明客户效果。'
  224. },
  225. {
  226. owner: '商务',
  227. title: '人工复核标签与归因指南',
  228. path: relIfExists(root, path.join(dirs.manualReviewGuide, 'manual-review-label-guide.md')),
  229. status: summaries.manualReviewGuide?.passed ? `标签 ${Number(summaries.manualReviewGuide.labelCount || 0)} 个` : '缺失或未生成',
  230. usage: '规范商务复核标签、负样本归因和客户反馈填写口径。',
  231. boundary: '标签指南只统一口径,不证明客户效果。'
  232. },
  233. {
  234. owner: '商务/技术/AI',
  235. title: '商务执行总表',
  236. path: relIfExists(root, path.join(dirs.businessExecution, 'business-execution-index.md')),
  237. status: summaries.businessExecution?.passed ? `rowCount=${Number(summaries.businessExecution.rowCount || 0)}` : '缺失或未生成',
  238. usage: '在一张表里查看负责人行动、修复清单、字段补齐负责人和复验命令。',
  239. boundary: '执行总表只组织工作流,不证明客户效果。'
  240. },
  241. {
  242. owner: '商务/技术/AI',
  243. title: '最新提号表单索引',
  244. path: relIfExists(root, path.join(dirs.latestFormIndex, 'latest-form-index.md')),
  245. status: summaries.latestFormIndex?.passed ? '已索引' : '缺失或未生成',
  246. usage: '快速定位最新补证表单、软件端候选名单、负责人行动文件和中文 SOP。',
  247. boundary: '索引入口不证明客户效果。'
  248. },
  249. {
  250. owner: '技术/AI',
  251. title: '优化完成度审计报告',
  252. path: relIfExists(root, path.join(dirs.optimizationCompletion, 'optimization-completion-report.md')),
  253. status: summaries.optimizationCompletion
  254. ? `readyForClaim=${Boolean(summaries.optimizationCompletion.readyForClaim)},proofGapOpenCount=${Number(summaries.optimizationCompletion.proofGapOpenCount || 0)}`
  255. : '缺失或未生成',
  256. usage: '复查长期优化是否具备完成声明条件,并汇总仍阻塞的真实证明缺口。',
  257. boundary: '完成度审计入口本身不证明客户效果;readyForClaim=false 时不得宣称长期优化完成。'
  258. },
  259. {
  260. owner: '技术/AI',
  261. title: '长跑前置门禁报告',
  262. path: relIfExists(root, path.join(dirs.longRunReadiness, 'long-run-readiness-report.md')),
  263. status: summaries.longRunReadiness
  264. ? `ready=${Boolean(summaries.longRunReadiness.ready)},failCount=${Number(summaries.longRunReadiness?.counts?.fail ?? summaries.longRunReadiness?.failCount ?? 0)}`
  265. : '缺失或未生成',
  266. usage: '复查 full-matrix/live 长跑是否具备启动条件,并定位 live preflight、历史数据、intake readiness 和 proof-gap closure 卡点。',
  267. boundary: '长跑前置门禁入口不证明客户效果;ready=false 时不能启动或宣称 full-matrix/live 长跑完成。'
  268. }
  269. ];
  270. }
  271. function buildFillRows(summaries) {
  272. const indexedRows = summaries.latestFormIndex?.businessFillGuide?.rows;
  273. const rows = Array.isArray(indexedRows) && indexedRows.length
  274. ? indexedRows
  275. : (summaries.intakeChecklist?.ownerGroups || []).map((group, index) => ({
  276. order: index + 1,
  277. owner: group.owner,
  278. section: group.section,
  279. file: (group.files || [])[0] || '',
  280. firstItemIds: group.firstItemIds || [],
  281. issueCount: Number(group.itemCount || 0),
  282. fillAction: group.nextAction || '',
  283. recheckCommand: '',
  284. acceptance: group.acceptance || '',
  285. boundary: group.boundary || '补齐材料,不证明客户效果'
  286. }));
  287. return rows.map((row, index) => {
  288. const section = row.section || '';
  289. const videoReady = section === 'video' &&
  290. Boolean(summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight) &&
  291. Number(summaries.videoReadiness?.failureCount || 0) === 0;
  292. const issueCount = videoReady ? 0 : Number(row.issueCount || 0);
  293. return {
  294. order: Number(row.order || index + 1),
  295. owner: row.owner || '',
  296. section,
  297. title: sectionTitle(section),
  298. file: normalizePath(row.file || ''),
  299. firstItemIds: Array.isArray(row.firstItemIds) ? row.firstItemIds : [],
  300. issueCount,
  301. status: videoReady ? '可进入视频 A/B 前置' : (issueCount > 0 ? '待补齐' : '待复验'),
  302. fillAction: videoReady ? '保持真实参考视频和真实候选视频证据链,下一步进入真实视频 A/B live proof。' : (row.fillAction || ''),
  303. recheckCommand: row.recheckCommand || '',
  304. acceptance: row.acceptance || '',
  305. boundary: row.boundary || '补齐材料,不证明客户效果。'
  306. };
  307. });
  308. }
  309. function buildProofGapRows(operatorPack, closure) {
  310. const closureRows = new Map((Array.isArray(closure?.rows) ? closure.rows : []).map(row => [row.id, row]));
  311. return (Array.isArray(operatorPack?.rows) ? operatorPack.rows : []).map(row => {
  312. const closureRow = closureRows.get(row.gapId) || {};
  313. const status = closureRow.status || row.status || 'open';
  314. const missingProofRequirements = Array.isArray(closureRow.missingProofRequirements)
  315. ? closureRow.missingProofRequirements
  316. : (Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements : []);
  317. return {
  318. gapId: row.gapId || '',
  319. title: row.title || closureRow.title || '',
  320. owner: row.owner || '',
  321. priority: Number(row.priority || 99),
  322. status,
  323. fillFiles: Array.isArray(row.fillFiles) ? row.fillFiles.map(normalizePath) : [],
  324. fields: Array.isArray(row.fields) ? row.fields : [],
  325. recheckCommands: Array.isArray(row.recheckCommands) ? row.recheckCommands : [],
  326. evidence: closureRow.evidence || row.evidence || '',
  327. missingProofRequirements,
  328. nextAction: status === 'closed'
  329. ? (closureRow.next || '保持证据链并纳入 evidence:index。')
  330. : (row.nextAction || closureRow.next || ''),
  331. acceptance: row.acceptance || closureRow.required || '',
  332. boundary: row.boundary || '补齐材料,不证明客户效果。'
  333. };
  334. }).sort((a, b) => a.priority - b.priority);
  335. }
  336. function expectedOwnerArtifactCount(operatorPack, ownerArtifacts) {
  337. if (Array.isArray(operatorPack?.ownerArtifacts) && operatorPack.ownerArtifacts.length) {
  338. return operatorPack.ownerArtifacts.length;
  339. }
  340. if (Number.isFinite(Number(operatorPack?.ownerArtifactCount)) && Number(operatorPack.ownerArtifactCount) > 0) {
  341. return Number(operatorPack.ownerArtifactCount);
  342. }
  343. if (Array.isArray(operatorPack?.ownerGroups) && operatorPack.ownerGroups.length) {
  344. return operatorPack.ownerGroups.length;
  345. }
  346. if (Array.isArray(ownerArtifacts) && ownerArtifacts.length) return ownerArtifacts.length;
  347. return 1;
  348. }
  349. function normalizeOperatorPackOwnerArtifacts(root, operatorPackDir, operatorPack) {
  350. if (!Array.isArray(operatorPack?.ownerArtifacts)) return [];
  351. return operatorPack.ownerArtifacts.map(item => ({
  352. owner: item.owner || '',
  353. actionCount: Number(item.actionCount || 0),
  354. topPriority: Number(item.topPriority || 0),
  355. markdown: relIfExists(root, resolveArtifactPath(root, operatorPackDir, item.markdown)),
  356. csv: relIfExists(root, resolveArtifactPath(root, operatorPackDir, item.csv)),
  357. gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
  358. evidenceRowCount: Number(item.evidenceRowCount || 0),
  359. nextActionCount: Number(item.nextActionCount || 0)
  360. })).filter(item => item.owner || item.markdown || item.csv);
  361. }
  362. function resolveArtifactPath(root, baseDir, value) {
  363. if (!value) return '';
  364. const text = String(value);
  365. const normalized = text.replace(/\\/g, '/');
  366. if (path.isAbsolute(text)) return text;
  367. if (normalized.startsWith('outputs/')) return path.join(root, normalized);
  368. return path.join(baseDir, text);
  369. }
  370. function buildRepairRows(root, dirs, summaries) {
  371. return [
  372. {
  373. owner: '商务',
  374. title: '历史数据/人工复核修复清单',
  375. csv: relIfExists(root, path.join(dirs.intakeReadiness, 'intake-readiness-repair-actions.csv')),
  376. issueCount: Number(summaries.intakeReadiness?.failureCount || 0),
  377. actionCount: Array.isArray(summaries.intakeReadiness?.repairActions) ? summaries.intakeReadiness.repairActions.length : 0,
  378. status: summaries.intakeReadiness?.acceptance?.overallReady ? '已就绪' : '待补齐',
  379. acceptance: 'intake:readiness overallReady=true 且 failureCount=0。',
  380. boundary: '修复清单只帮助补齐材料,不证明客户效果。'
  381. },
  382. {
  383. owner: '商务/投放',
  384. title: '视频资源修复清单',
  385. csv: relIfExists(root, path.join(dirs.videoReadiness, 'video-resource-repair-actions.csv')),
  386. issueCount: Number(summaries.videoReadiness?.failureCount || 0),
  387. actionCount: Array.isArray(summaries.videoReadiness?.repairActions) ? summaries.videoReadiness.repairActions.length : 0,
  388. status: summaries.videoReadiness?.acceptance?.readyForVideoAbPreflight ? '可进入视频 A/B 前置' : '待补视频资源',
  389. acceptance: 'video:resource-readiness readyForVideoAbPreflight=true 且 failureCount=0。',
  390. boundary: '视频修复清单不证明视频分析提升提号率。'
  391. },
  392. {
  393. owner: '商务/投放',
  394. title: '主页近期内容证据修复清单',
  395. csv: relIfExists(root, path.join(dirs.homepageReadiness, 'homepage-evidence-repair-actions.csv')),
  396. issueCount: Number(summaries.homepageReadiness?.failureCount || 0),
  397. actionCount: Array.isArray(summaries.homepageReadiness?.repairActions) ? summaries.homepageReadiness.repairActions.length : 0,
  398. status: summaries.homepageReadiness?.ready ? '主页证据已就绪' : '待补主页证据',
  399. acceptance: 'homepage:readiness ready=true,且强推荐具备近期内容证据。',
  400. boundary: '主页证据修复清单不证明客户效果。'
  401. }
  402. ];
  403. }
  404. function buildRecheckCommands(summaries) {
  405. const commands = [];
  406. for (const item of summaries.intakeChecklist?.recheckCommands || []) {
  407. commands.push({
  408. stage: item.stage || '',
  409. owner: item.owner || '技术/AI',
  410. command: item.command || '',
  411. acceptance: item.acceptance || '',
  412. boundary: item.boundary || '复验通过不等于客户效果证明。'
  413. });
  414. }
  415. for (const row of summaries.operatorPack?.rows || []) {
  416. for (const command of row.recheckCommands || []) {
  417. commands.push({
  418. stage: row.title || row.gapId || '证明缺口复验',
  419. owner: row.owner || '技术/AI',
  420. command,
  421. acceptance: row.acceptance || '',
  422. boundary: row.boundary || '复验通过不等于客户效果证明。'
  423. });
  424. }
  425. }
  426. commands.push(
  427. {
  428. stage: '长跑前置门禁',
  429. owner: '技术/AI',
  430. command: 'npm run longrun:readiness -- --mode full-matrix --intake-readiness outputs\\intake-readiness-latest\\intake-readiness-summary.json --proof-gap outputs\\proof-gap-request-latest\\proof-gap-request-summary.json --proof-gap-closure outputs\\proof-gap-closure-latest\\proof-gap-closure-summary.json --output outputs\\long-run-readiness-latest',
  431. acceptance: 'ready=true 且 failCount=0 后,才允许进入真实 live/provider 长跑启动判断。',
  432. boundary: 'ready=false 时不能启动或宣称 full-matrix/live 长跑完成。'
  433. },
  434. {
  435. stage: '优化完成度审计',
  436. owner: '技术/AI',
  437. command: 'npm run optimization:completion -- --output outputs\\optimization-completion-latest',
  438. acceptance: 'complete=true、readyForClaim=true、proofGapOpenCount=0 才能进入完成判断。',
  439. boundary: 'readyForClaim=false 时不得宣称长期优化完成。'
  440. },
  441. {
  442. stage: '真实材料一键审计 Pipeline',
  443. owner: '技术/AI',
  444. command: 'npm run optimization:pipeline -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict',
  445. acceptance: '真实历史数据、视频资源、复核指标和客户效果审计按 pipeline 通过。',
  446. boundary: 'pipeline 未关闭 proof-gap:closure 前不得宣称客户效果。'
  447. },
  448. {
  449. stage: '每轮沉淀刷新',
  450. owner: '技术/AI',
  451. command: 'npm run round:refresh -- --output-root outputs --strict',
  452. acceptance: 'round-deposition.passed=true 且 failCount=0。',
  453. boundary: '刷新沉淀不证明客户效果。'
  454. }
  455. );
  456. return dedupeBy(commands.filter(item => item.command), item => item.command);
  457. }
  458. function exportRow(type, order, owner, title, target, status, acceptance, boundary, issueCount = '', evidence = '', nextAction = '') {
  459. return {
  460. type,
  461. order,
  462. owner: owner || '',
  463. title: title || '',
  464. target: normalizePath(target || ''),
  465. status: status || '',
  466. issueCount,
  467. evidence: evidence || '',
  468. nextAction: nextAction || '',
  469. acceptance: acceptance || '',
  470. boundary: boundary || '不证明客户效果。'
  471. };
  472. }
  473. function renderReport(summary) {
  474. const lines = [
  475. '# 真实验收材料收集总包',
  476. '',
  477. `- 生成时间:${summary.generatedAt}`,
  478. `- 是否完成长期目标:${summary.complete ? '是' : '否'}`,
  479. `- proofOpenCount:${summary.proofOpenCount}`,
  480. `- operatorPackEvidenceRowCount:${summary.operatorPackEvidenceRowCount}`,
  481. `- operatorPackNextActionCount:${summary.operatorPackNextActionCount}`,
  482. `- operatorPackMissingProofRequirementCount:${summary.operatorPackMissingProofRequirementCount}`,
  483. `- operatorPackOwnerArtifactCount:${summary.operatorPackOwnerArtifactCount}`,
  484. `- 用途:把真实材料模板、字段缺口、修复清单、证明缺口和复验命令集中成一个商务可打开的入口;不证明客户效果。`,
  485. '',
  486. '## 打开顺序',
  487. '',
  488. '| 负责人 | 文件 | 状态 | 用法 | 边界 |',
  489. '| --- | --- | --- | --- | --- |',
  490. ...summary.templateFiles.map(item => tableRow([item.owner, item.path || '缺失', item.status, item.usage, item.boundary])),
  491. '',
  492. '## 操作包负责人附件',
  493. '',
  494. '| 负责人 | 动作数 | Markdown | CSV | 缺口 | 当前证据行 | 下一步动作 |',
  495. '| --- | ---: | --- | --- | --- | ---: | ---: |',
  496. ...summary.operatorPackOwnerArtifacts.map(item => tableRow([
  497. item.owner,
  498. item.actionCount,
  499. item.markdown || '缺失',
  500. item.csv || '缺失',
  501. item.gapIds.join(';') || '缺失',
  502. item.evidenceRowCount,
  503. item.nextActionCount
  504. ])),
  505. '',
  506. '## 商务填表顺序',
  507. '',
  508. '| 顺序 | 负责人 | 区域 | 文件 | 优先项 | 问题数 | 填表动作 | 补齐后复验 | 验收 | 边界 |',
  509. '| ---: | --- | --- | --- | --- | ---: | --- | --- | --- | --- |',
  510. ...summary.fillRows.map(row => tableRow([
  511. row.order,
  512. row.owner,
  513. row.title,
  514. row.file || '缺失',
  515. row.firstItemIds.join('、') || '无',
  516. row.issueCount,
  517. row.fillAction,
  518. row.recheckCommand || '补齐后运行 round:refresh',
  519. row.acceptance,
  520. row.boundary
  521. ])),
  522. '',
  523. '## 证明缺口闭环',
  524. '',
  525. '| 优先级 | 缺口 | 负责人 | 状态 | 填写文件 | 当前证据 | 下一步 | 待补字段/证明 | 通过标准 | 边界 |',
  526. '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
  527. ...summary.proofGapRows.map(row => tableRow([
  528. row.priority,
  529. `${row.title} (${row.gapId})`,
  530. row.owner,
  531. row.status,
  532. row.fillFiles.join(';') || '缺失',
  533. row.evidence || '缺失',
  534. row.nextAction || '按操作包补齐',
  535. row.fields.join(';') || '按操作包补齐',
  536. row.acceptance,
  537. row.boundary
  538. ])),
  539. '',
  540. '## 修复清单入口',
  541. '',
  542. '| 负责人 | 修复清单 | 当前问题数 | 修复动作数 | 状态 | 验收 | 边界 |',
  543. '| --- | --- | ---: | ---: | --- | --- | --- |',
  544. ...summary.repairRows.map(row => tableRow([row.owner, row.csv || '缺失', row.issueCount, row.actionCount, row.status, row.acceptance, row.boundary])),
  545. '',
  546. '## 补齐后复验命令',
  547. '',
  548. '| 阶段 | 负责人 | 命令 | 验收 | 边界 |',
  549. '| --- | --- | --- | --- | --- |',
  550. ...summary.recheckCommands.map(item => tableRow([item.stage, item.owner, item.command, item.acceptance, item.boundary])),
  551. '',
  552. '## 防误用边界',
  553. '',
  554. ...summary.guardrails.map(item => `- ${item}`)
  555. ];
  556. return lines.join('\n');
  557. }
  558. function renderCsv(rows) {
  559. const headers = ['类型', '顺序', '负责人', '标题', '文件或命令', '状态', '问题数', '当前证据', '下一步', '验收标准', '边界'];
  560. return [
  561. headers.join(','),
  562. ...rows.map(row => [
  563. row.type,
  564. row.order,
  565. row.owner,
  566. row.title,
  567. row.target,
  568. row.status,
  569. row.issueCount,
  570. row.evidence,
  571. row.nextAction,
  572. row.acceptance,
  573. row.boundary
  574. ].map(csvCell).join(','))
  575. ].join('\n');
  576. }
  577. function sectionTitle(section) {
  578. if (section === 'history') return '历史 Brief 数据';
  579. if (section === 'review') return '人工复核和客户选择';
  580. if (section === 'video') return '视频资源证据';
  581. return section || '真实材料';
  582. }
  583. function dedupeBy(items, keyFn) {
  584. const seen = new Set();
  585. const result = [];
  586. for (const item of items) {
  587. const key = keyFn(item);
  588. if (seen.has(key)) continue;
  589. seen.add(key);
  590. result.push(item);
  591. }
  592. return result;
  593. }
  594. function countRowsWith(rows, predicate) {
  595. return rows.reduce((count, row) => count + (predicate(row) ? 1 : 0), 0);
  596. }
  597. function pickDir(outputsDir, pattern) {
  598. if (!fs.existsSync(outputsDir)) return '';
  599. const latest = fs.readdirSync(outputsDir, { withFileTypes: true })
  600. .filter(entry => entry.isDirectory() && pattern.test(entry.name))
  601. .map(entry => path.join(outputsDir, entry.name))
  602. .filter(fs.existsSync)
  603. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
  604. return latest || '';
  605. }
  606. function readJson(file) {
  607. if (!file || !fs.existsSync(file)) return null;
  608. try {
  609. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  610. } catch {
  611. return null;
  612. }
  613. }
  614. function relIfExists(root, file) {
  615. return file && fs.existsSync(file) ? path.relative(root, file).replace(/\\/g, '/') : '';
  616. }
  617. function normalizePath(value) {
  618. return String(value || '').replace(/\\/g, '/');
  619. }
  620. function tableRow(cells) {
  621. return `| ${cells.map(cell => String(cell ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ')).join(' | ')} |`;
  622. }
  623. function csvCell(value) {
  624. const text = String(value ?? '').replace(/\r?\n/g, ' ');
  625. return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  626. }
  627. function parseArgs(argv) {
  628. const result = {};
  629. for (let index = 0; index < argv.length; index += 1) {
  630. const arg = argv[index];
  631. if (!arg.startsWith('--')) continue;
  632. const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  633. const next = argv[index + 1];
  634. if (!next || next.startsWith('--')) result[key] = true;
  635. else {
  636. result[key] = next;
  637. index += 1;
  638. }
  639. }
  640. return result;
  641. }
  642. function withBom(text) {
  643. return `\uFEFF${text}`;
  644. }
  645. if (require.main === module) main();
  646. module.exports = {
  647. buildRealProofIntakeBundle
  648. };