latest-form-index.js 72 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325
  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', 'latest-form-index-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 = buildLatestFormIndex({ root: ROOT, outputsDir });
  11. fs.mkdirSync(outputDir, { recursive: true });
  12. const summaryPath = path.join(outputDir, 'latest-form-index-summary.json');
  13. const reportPath = path.join(outputDir, 'latest-form-index.md');
  14. fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
  15. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  16. console.log(JSON.stringify({
  17. outputDir,
  18. summary: summaryPath,
  19. report: reportPath,
  20. passed: summary.passed,
  21. primaryForm: summary.primaryForm?.csv || null,
  22. clientList: summary.clientList?.csv || null
  23. }, null, 2));
  24. if (args.strict && !summary.passed) process.exitCode = 1;
  25. }
  26. function buildLatestFormIndex({ root, outputsDir }) {
  27. const proofFormDir = latestOutputDir(outputsDir, /^tihao-proof-gap-software-form-/) ||
  28. path.join(outputsDir, 'tihao-proof-gap-software-form-latest');
  29. const clientListDir = latestOutputDir(outputsDir, /^software-client-/) ||
  30. path.join(outputsDir, 'software-client-latest');
  31. const nextActionsDir = latestOutputDir(outputsDir, /^business-proof-next-actions-/) ||
  32. path.join(outputsDir, 'business-proof-next-actions-latest');
  33. const handoffDir = latestOutputDir(outputsDir, /^optimization-handoff-/) ||
  34. path.join(outputsDir, 'optimization-handoff-latest');
  35. const intakeReadinessDir = latestOutputDir(outputsDir, /^intake-readiness-/) ||
  36. path.join(outputsDir, 'intake-readiness-latest');
  37. const videoResourceReadinessDir = latestOutputDir(outputsDir, /^video-resource-readiness-/) ||
  38. path.join(outputsDir, 'video-resource-readiness-latest');
  39. const homepageEvidenceReadinessDir = latestOutputDir(outputsDir, /^homepage-evidence-readiness-/) ||
  40. path.join(outputsDir, 'homepage-evidence-readiness-latest');
  41. const longRunReadinessDir = latestOutputDir(outputsDir, /^long-run-readiness-/) ||
  42. path.join(outputsDir, 'long-run-readiness-latest');
  43. const intakeFieldChecklistDir = latestOutputDir(outputsDir, /^intake-field-checklist-/) ||
  44. path.join(outputsDir, 'intake-field-checklist-latest');
  45. const manualReviewLabelGuideDir = latestOutputDir(outputsDir, /^manual-review-label-guide-/) ||
  46. path.join(outputsDir, 'manual-review-label-guide-latest');
  47. const businessExecutionIndexDir = latestOutputDir(outputsDir, /^business-execution-index-/) ||
  48. path.join(outputsDir, 'business-execution-index-latest');
  49. const planExecutionStatusDir = latestOutputDir(outputsDir, /^plan-execution-status-/) ||
  50. path.join(outputsDir, 'plan-execution-status-latest');
  51. const proofGapRequestDir = latestOutputDir(outputsDir, /^proof-gap-request-/) ||
  52. path.join(outputsDir, 'proof-gap-request-latest');
  53. const proofGapClosureDir = latestOutputDir(outputsDir, /^proof-gap-closure-/) ||
  54. path.join(outputsDir, 'proof-gap-closure-latest');
  55. const proofGapOperatorPackDir = latestOutputDir(outputsDir, /^proof-gap-operator-pack-/) ||
  56. path.join(outputsDir, 'proof-gap-operator-pack-latest');
  57. const realProofIntakeBundleDir = latestOutputDir(outputsDir, /^real-proof-intake-bundle-/) ||
  58. path.join(outputsDir, 'real-proof-intake-bundle-latest');
  59. const realProofClosureWorkOrderDir = latestOutputDir(outputsDir, /^real-proof-closure-work-order-/) ||
  60. path.join(outputsDir, 'real-proof-closure-work-order-latest');
  61. const optimizationCompletionDir = latestOutputDir(outputsDir, /^optimization-completion-/) ||
  62. path.join(outputsDir, 'optimization-completion-latest');
  63. const localSeedToIntakeWorklistDir = latestOutputDir(outputsDir, /^local-seed-to-intake-worklist-/) ||
  64. path.join(outputsDir, 'local-seed-to-intake-worklist-latest');
  65. const realGapMaterialAuditDir = latestOutputDir(outputsDir, /^real-gap-material-audit-/) ||
  66. path.join(outputsDir, 'real-gap-material-audit-latest');
  67. const proofSummary = readJsonIfExists(path.join(proofFormDir, 'tihao-proof-gap-software-form-summary.json'));
  68. const clientSummary = readJsonIfExists(path.join(clientListDir, 'software-client-list.final.summary.json'));
  69. const nextActionsSummary = readJsonIfExists(path.join(nextActionsDir, 'business-proof-next-actions-summary.json'));
  70. const handoffSummary = readJsonIfExists(path.join(handoffDir, 'optimization-handoff-summary.json'));
  71. const intakeReadinessSummary = readJsonIfExists(path.join(intakeReadinessDir, 'intake-readiness-summary.json'));
  72. const videoResourceReadinessSummary = readJsonIfExists(path.join(videoResourceReadinessDir, 'video-resource-readiness-summary.json'));
  73. const homepageEvidenceReadinessSummary = readJsonIfExists(path.join(homepageEvidenceReadinessDir, 'homepage-evidence-readiness-summary.json'));
  74. const longRunReadinessSummary = readJsonIfExists(path.join(longRunReadinessDir, 'long-run-readiness-summary.json'));
  75. const intakeFieldChecklistSummary = readJsonIfExists(path.join(intakeFieldChecklistDir, 'intake-field-checklist-summary.json'));
  76. const manualReviewLabelGuideSummary = readJsonIfExists(path.join(manualReviewLabelGuideDir, 'manual-review-label-guide-summary.json'));
  77. const businessExecutionIndexSummary = readJsonIfExists(path.join(businessExecutionIndexDir, 'business-execution-index-summary.json'));
  78. const planExecutionStatusSummary = readJsonIfExists(path.join(planExecutionStatusDir, 'plan-execution-status-summary.json'));
  79. const proofGapRequestSummary = readJsonIfExists(path.join(proofGapRequestDir, 'proof-gap-request-summary.json'));
  80. const proofGapClosureSummary = readJsonIfExists(path.join(proofGapClosureDir, 'proof-gap-closure-summary.json'));
  81. const proofGapOperatorPackSummary = readJsonIfExists(path.join(proofGapOperatorPackDir, 'proof-gap-operator-pack-summary.json'));
  82. const realProofIntakeBundleSummary = readJsonIfExists(path.join(realProofIntakeBundleDir, 'real-proof-intake-bundle-summary.json'));
  83. const realProofClosureWorkOrderSummary = readJsonIfExists(path.join(realProofClosureWorkOrderDir, 'real-proof-closure-work-order-summary.json'));
  84. const optimizationCompletionSummary = readJsonIfExists(path.join(optimizationCompletionDir, 'optimization-completion-summary.json'));
  85. const localSeedToIntakeWorklistSummary = readJsonIfExists(path.join(localSeedToIntakeWorklistDir, 'local-seed-to-intake-worklist-summary.json'));
  86. const realGapMaterialAuditSummary = readJsonIfExists(path.join(realGapMaterialAuditDir, 'real-gap-material-audit-summary.json'));
  87. const primaryForm = fileGroup(root, proofFormDir, {
  88. csv: 'tihao-proof-gap-software-form.csv',
  89. markdown: 'tihao-proof-gap-software-form.md',
  90. summary: 'tihao-proof-gap-software-form-summary.json'
  91. });
  92. const clientList = fileGroup(root, clientListDir, {
  93. csv: 'software-client-list.final.csv',
  94. markdown: 'software-client-list.final.md',
  95. summary: 'software-client-list.final.summary.json'
  96. });
  97. const intakeFieldChecklist = fileGroup(root, intakeFieldChecklistDir, {
  98. csv: 'intake-field-checklist.csv',
  99. markdown: 'intake-field-checklist.md',
  100. summary: 'intake-field-checklist-summary.json'
  101. });
  102. const manualReviewLabelGuide = fileGroup(root, manualReviewLabelGuideDir, {
  103. csv: 'manual-review-label-guide.csv',
  104. markdown: 'manual-review-label-guide.md',
  105. summary: 'manual-review-label-guide-summary.json'
  106. });
  107. const businessExecutionIndex = fileGroup(root, businessExecutionIndexDir, {
  108. csv: 'business-execution-index.csv',
  109. markdown: 'business-execution-index.md',
  110. summary: 'business-execution-index-summary.json'
  111. });
  112. const planExecutionStatus = fileGroup(root, planExecutionStatusDir, {
  113. csv: 'plan-execution-status.csv',
  114. markdown: 'plan-execution-status.md',
  115. summary: 'plan-execution-status-summary.json'
  116. });
  117. const proofGapRequest = fileGroup(root, proofGapRequestDir, {
  118. csv: 'proof-gap-request-table.csv',
  119. markdown: 'proof-gap-request-report.md',
  120. summary: 'proof-gap-request-summary.json'
  121. });
  122. const proofGapClosure = fileGroup(root, proofGapClosureDir, {
  123. markdown: 'proof-gap-closure-report.md',
  124. summary: 'proof-gap-closure-summary.json'
  125. });
  126. const proofGapOperatorPack = fileGroup(root, proofGapOperatorPackDir, {
  127. csv: 'proof-gap-operator-pack.csv',
  128. markdown: 'proof-gap-operator-pack.md',
  129. summary: 'proof-gap-operator-pack-summary.json'
  130. });
  131. const realProofIntakeBundle = fileGroup(root, realProofIntakeBundleDir, {
  132. csv: 'real-proof-intake-bundle.csv',
  133. markdown: 'real-proof-intake-bundle.md',
  134. summary: 'real-proof-intake-bundle-summary.json'
  135. });
  136. const realProofClosureWorkOrder = fileGroup(root, realProofClosureWorkOrderDir, {
  137. csv: 'real-proof-closure-work-order.csv',
  138. markdown: 'real-proof-closure-work-order.md',
  139. summary: 'real-proof-closure-work-order-summary.json'
  140. });
  141. const optimizationCompletion = fileGroup(root, optimizationCompletionDir, {
  142. markdown: 'optimization-completion-report.md',
  143. summary: 'optimization-completion-summary.json'
  144. });
  145. const localSeedToIntakeWorklist = fileGroup(root, localSeedToIntakeWorklistDir, {
  146. markdown: 'local-seed-to-intake-worklist.md',
  147. csv: 'local-seed-to-intake-worklist.csv',
  148. summary: 'local-seed-to-intake-worklist-summary.json',
  149. historyDraft: 'history-intake-draft.csv',
  150. videoWorklist: 'video-resource-worklist.csv',
  151. candidateWorklist: 'candidate-seed-worklist.csv'
  152. });
  153. const realGapMaterialAudit = fileGroup(root, realGapMaterialAuditDir, {
  154. markdown: 'real-gap-material-audit.md',
  155. csv: 'real-gap-material-audit.csv',
  156. summary: 'real-gap-material-audit-summary.json'
  157. });
  158. const longRunReadiness = fileGroup(root, longRunReadinessDir, {
  159. report: 'long-run-readiness-report.md',
  160. summary: 'long-run-readiness-summary.json'
  161. });
  162. const ownerArtifacts = normalizeOwnerArtifacts(root, nextActionsDir, nextActionsSummary);
  163. const proofGapOperatorOwnerArtifacts = normalizeOwnerArtifacts(root, proofGapOperatorPackDir, proofGapOperatorPackSummary);
  164. const realProofOperatorPackOwnerArtifacts = normalizePassThroughOwnerArtifacts(root, realProofIntakeBundleSummary?.operatorPackOwnerArtifacts || []);
  165. const businessExecutionOperatorPackOwnerArtifacts = normalizePassThroughOwnerArtifacts(root, businessExecutionIndexSummary?.operatorPackOwnerArtifacts || []);
  166. const planExecutionOperatorPackOwnerArtifacts = normalizePassThroughOwnerArtifacts(root, planExecutionStatusSummary?.operatorPackOwnerArtifacts || []);
  167. const intakeOwnerGroups = normalizeIntakeOwnerGroups(root, intakeFieldChecklistSummary?.ownerGroups || []);
  168. const intakeRecheckCommands = normalizeRecheckCommands(intakeFieldChecklistSummary?.recheckCommands || []);
  169. const businessFillGuide = buildBusinessFillGuide({
  170. ownerGroups: intakeOwnerGroups,
  171. recheckCommands: intakeRecheckCommands
  172. });
  173. const proofOpenCount = Number(nextActionsSummary?.sourceState?.proofGapOpenCount ?? handoffSummary?.businessProofNextActions?.actionCount ?? 0);
  174. const proofGapClosureCounts = normalizeProofGapClosureCounts(proofGapClosureSummary);
  175. const actionCount = Number(nextActionsSummary?.actionCount ?? (Array.isArray(nextActionsSummary?.actions) ? nextActionsSummary.actions.length : 0));
  176. const nextActionSourceState = normalizeBusinessProofNextActionSourceState(nextActionsSummary?.sourceState);
  177. const nextActionTopActions = normalizeBusinessProofNextTopActions(
  178. Array.isArray(nextActionsSummary?.topActions) && nextActionsSummary.topActions.length
  179. ? nextActionsSummary.topActions
  180. : nextActionsSummary?.actions
  181. );
  182. const primaryPassed = Boolean(proofSummary?.passed);
  183. const clientExists = fs.existsSync(path.join(clientListDir, 'software-client-list.final.csv'));
  184. const passed = primaryPassed && Boolean(primaryForm.csv) && actionCount >= 1;
  185. const supportDocs = [
  186. {
  187. title: '长期优化计划主文件',
  188. path: relIfExists(root, path.join(root, 'docs', 'tihao-experience-optimization-plan.md')),
  189. usage: '当前目标文件;用于执行长期优化计划、同步每轮进度、确认验收标准和完成边界。'
  190. },
  191. {
  192. title: '长期优化计划中文执行版',
  193. path: relIfExists(root, path.join(root, 'docs', 'tihao-experience-optimization-plan.zh-CN.md')),
  194. usage: '用于下一轮 AI 或项目负责人理解长期目标、策略矩阵、验收标准和完成边界。'
  195. },
  196. {
  197. title: '补证操作顺序中文执行版',
  198. path: relIfExists(root, path.join(root, 'docs', 'business-proof-action-order.zh-CN.md')),
  199. usage: '用于商务、投放、技术/AI 按 15 步顺序补齐真实材料和验收证据。'
  200. }
  201. ,
  202. {
  203. title: '提号任务交接入口',
  204. path: relIfExists(root, path.join(root, 'docs', 'tihao-handoff-index.md')),
  205. usage: '下一轮 AI、商务或投放接手时优先打开;按顺序定位主计划、补证顺序、最新表单索引、真实材料总包、proof-gap 请求包、商务执行总表和交接摘要。'
  206. }
  207. ];
  208. return {
  209. generatedAt: new Date().toISOString(),
  210. root,
  211. outputsDir,
  212. passed,
  213. purpose: '给商务和下一轮 AI 快速定位最新提号补证表单、候选名单、负责人行动文件和验收边界。',
  214. primaryForm: {
  215. title: '最新提号补证表单',
  216. usage: '用于分派真实历史 Brief、真实视频资源、视频 A/B、live 长跑和客户效果审计补证任务。',
  217. directCustomerProof: false,
  218. csv: primaryForm.csv,
  219. markdown: primaryForm.markdown,
  220. summary: primaryForm.summary,
  221. generatedAt: proofSummary?.generatedAt || null,
  222. passed: primaryPassed,
  223. rowCount: Number(proofSummary?.rowCount || 0),
  224. duplicateIdCount: Number(proofSummary?.duplicateIdCount || 0),
  225. headerMatches: Boolean(proofSummary?.headerMatches),
  226. rowWidthOk: Boolean(proofSummary?.rowWidthOk)
  227. },
  228. clientList: {
  229. title: '最新软件端候选名单',
  230. usage: '用于商务复核候选博主;当前仍需结合风险提示和人工复核标签,不等同于客户效果证明。',
  231. directCustomerProof: false,
  232. exists: clientExists,
  233. csv: clientList.csv,
  234. markdown: clientList.markdown,
  235. summary: clientList.summary,
  236. generatedAt: clientSummary?.generatedAt || null,
  237. rowCount: Number(clientSummary?.rowCount || clientSummary?.finalRowCount || clientSummary?.outputRows || 0),
  238. duplicateGroups: clientSummary?.duplicateGroups || clientSummary?.duplicateSummary || null,
  239. duplicateCount: Number(clientSummary?.duplicateCount || 0),
  240. duplicateCountMeaning: clientSummary?.duplicateCountMeaning || 'source_duplicate_removed_count',
  241. sourceDuplicateRemovedCount: Number(clientSummary?.sourceDuplicateRemovedCount ?? clientSummary?.duplicateCount ?? 0),
  242. finalDuplicateGroupCount: Number(clientSummary?.finalDuplicateGroupCount ?? finalDuplicateGroupCount(clientSummary?.duplicateAudit) ?? 0),
  243. rankContinuous: Boolean(clientSummary?.rankContinuous),
  244. dedupeRule: clientSummary?.dedupeRule || ''
  245. },
  246. intakeFieldChecklist: {
  247. title: '真实材料字段级补齐核对表',
  248. usage: '用于商务、投放按行号和字段补齐真实历史 Brief、人工复核字段和真实视频资源;不等同于客户效果证明。',
  249. directCustomerProof: false,
  250. exists: Boolean(intakeFieldChecklist.markdown || intakeFieldChecklist.csv),
  251. csv: intakeFieldChecklist.csv,
  252. markdown: intakeFieldChecklist.markdown,
  253. summary: intakeFieldChecklist.summary,
  254. generatedAt: intakeFieldChecklistSummary?.generatedAt || null,
  255. itemCount: Number(intakeFieldChecklistSummary?.itemCount || 0),
  256. missingRequiredCount: Number(intakeFieldChecklistSummary?.missingRequiredCount || 0),
  257. placeholderCount: Number(intakeFieldChecklistSummary?.placeholderCount || 0),
  258. ownerGroups: intakeOwnerGroups,
  259. recheckCommands: intakeRecheckCommands,
  260. passed: Boolean(intakeFieldChecklistSummary?.passed)
  261. },
  262. manualReviewLabelGuide: {
  263. title: '人工复核标签与归因指南',
  264. usage: '用于商务填写人工复核标签、负样本归因、反馈原因和客户选择;它只规范复核口径,不等同于客户效果证明。',
  265. directCustomerProof: false,
  266. proofLevel: manualReviewLabelGuideSummary?.proofLevel || 'smoke_or_local',
  267. exists: Boolean(manualReviewLabelGuide.markdown || manualReviewLabelGuide.csv),
  268. csv: manualReviewLabelGuide.csv,
  269. markdown: manualReviewLabelGuide.markdown,
  270. summary: manualReviewLabelGuide.summary,
  271. generatedAt: manualReviewLabelGuideSummary?.generatedAt || null,
  272. passed: Boolean(manualReviewLabelGuideSummary?.passed),
  273. labelCount: Number(manualReviewLabelGuideSummary?.labelCount || 0),
  274. positiveLabelCount: Number(manualReviewLabelGuideSummary?.positiveLabelCount || 0),
  275. negativeLabelCount: Number(manualReviewLabelGuideSummary?.negativeLabelCount || 0),
  276. attributionTypeCount: Number(manualReviewLabelGuideSummary?.attributionTypeCount || 0),
  277. negativeLabelsRequireAttribution: Boolean(manualReviewLabelGuideSummary?.negativeLabelsRequireAttribution)
  278. },
  279. businessFillGuide,
  280. businessExecutionIndex: {
  281. title: '商务补证执行总表',
  282. usage: '用于商务、投放和技术/AI 在一张表里查看负责人行动、修复清单、操作包负责人附件、字段补齐负责人、补齐后复验命令和关键文件;不等同于客户效果证明。',
  283. directCustomerProof: false,
  284. exists: Boolean(businessExecutionIndex.markdown || businessExecutionIndex.csv),
  285. csv: businessExecutionIndex.csv,
  286. markdown: businessExecutionIndex.markdown,
  287. summary: businessExecutionIndex.summary,
  288. generatedAt: businessExecutionIndexSummary?.generatedAt || null,
  289. passed: Boolean(businessExecutionIndexSummary?.passed),
  290. rowCount: Number(businessExecutionIndexSummary?.rowCount || 0),
  291. actionRowCount: Number(businessExecutionIndexSummary?.actionRowCount || 0),
  292. repairRowCount: Number(businessExecutionIndexSummary?.repairRowCount || 0),
  293. operatorPackOwnerArtifactRowCount: Number(businessExecutionIndexSummary?.operatorPackOwnerArtifactRowCount || 0),
  294. operatorPackOwnerArtifactCount: Number(businessExecutionIndexSummary?.operatorPackOwnerArtifactCount || businessExecutionOperatorPackOwnerArtifacts.length || 0),
  295. operatorPackOwnerArtifacts: businessExecutionOperatorPackOwnerArtifacts,
  296. fieldOwnerRowCount: Number(businessExecutionIndexSummary?.fieldOwnerRowCount || 0),
  297. recheckCommandRowCount: Number(businessExecutionIndexSummary?.recheckCommandRowCount || 0),
  298. proofOpenCount: Number(businessExecutionIndexSummary?.proofOpenCount || 0)
  299. },
  300. planExecutionStatus: {
  301. title: '长期优化计划执行状态面板',
  302. usage: '用于商务和下一轮 AI 在一张表里查看长期优化计划 8 个章节的执行状态、当前证据、下一步、验收标准和证明边界;不等同于客户效果证明。',
  303. directCustomerProof: false,
  304. exists: Boolean(planExecutionStatus.markdown || planExecutionStatus.csv),
  305. csv: planExecutionStatus.csv,
  306. markdown: planExecutionStatus.markdown,
  307. summary: planExecutionStatus.summary,
  308. generatedAt: planExecutionStatusSummary?.generatedAt || null,
  309. passed: Boolean(planExecutionStatusSummary?.passed),
  310. complete: Boolean(planExecutionStatusSummary?.complete),
  311. rowCount: Number(planExecutionStatusSummary?.rowCount || 0),
  312. proofOpenCount: Number(planExecutionStatusSummary?.proofOpenCount || 0),
  313. operatorPackOwnerArtifactCount: Number(planExecutionStatusSummary?.operatorPackOwnerArtifactCount || planExecutionOperatorPackOwnerArtifacts.length || 0),
  314. operatorPackOwnerArtifacts: planExecutionOperatorPackOwnerArtifacts,
  315. businessExecutionOperatorPackOwnerArtifactCount: Number(planExecutionStatusSummary?.businessExecutionOperatorPackOwnerArtifactCount || 0),
  316. businessExecutionOperatorPackOwnerArtifactRowCount: Number(planExecutionStatusSummary?.businessExecutionOperatorPackOwnerArtifactRowCount || 0),
  317. roundPassed: Boolean(planExecutionStatusSummary?.roundPassed),
  318. guardrailCount: Array.isArray(planExecutionStatusSummary?.guardrails) ? planExecutionStatusSummary.guardrails.length : 0
  319. },
  320. proofGapRequest: {
  321. title: '真实证明缺口请求包',
  322. usage: '用于把仍未关闭的真实证明缺口整理成对商务、投放和技术/AI 的请求清单;只请求补证,不证明客户效果。',
  323. directCustomerProof: false,
  324. proofLevel: 'smoke_or_local',
  325. exists: Boolean(proofGapRequest.markdown || proofGapRequest.csv),
  326. csv: proofGapRequest.csv,
  327. markdown: proofGapRequest.markdown,
  328. summary: proofGapRequest.summary,
  329. generatedAt: proofGapRequestSummary?.generatedAt || null,
  330. complete: Boolean(proofGapRequestSummary?.complete),
  331. unresolvedCount: Number(proofGapRequestSummary?.unresolvedCount || 0),
  332. rowCount: Array.isArray(proofGapRequestSummary?.rows) ? proofGapRequestSummary.rows.length : 0,
  333. proofGapClosureOpenCount: Number(proofGapRequestSummary?.observed?.proofGapClosureOpenCount || 0)
  334. },
  335. proofGapClosure: {
  336. title: '提号真实证明缺口关闭审计',
  337. usage: '用于判断真实业务证明缺口是否关闭;openCount/businessOpenCount 只统计业务缺口,intake 预审行用 precheckOpenCount 单独记录。',
  338. directCustomerProof: false,
  339. proofLevel: proofGapClosureSummary?.complete ? 'real_evidence' : 'not_business_proof',
  340. exists: Boolean(proofGapClosure.markdown || proofGapClosure.summary),
  341. markdown: proofGapClosure.markdown,
  342. summary: proofGapClosure.summary,
  343. generatedAt: proofGapClosureSummary?.generatedAt || null,
  344. complete: Boolean(proofGapClosureSummary?.complete),
  345. intakeReady: Boolean(proofGapClosureSummary?.intakeReady),
  346. openCount: Number(proofGapClosureSummary?.openCount || 0),
  347. closedCount: Number(proofGapClosureSummary?.closedCount || 0),
  348. businessOpenCount: proofGapClosureCounts.businessOpenCount,
  349. businessClosedCount: proofGapClosureCounts.businessClosedCount,
  350. businessGapCount: proofGapClosureCounts.businessGapCount,
  351. totalRows: proofGapClosureCounts.totalRows,
  352. precheckRowCount: proofGapClosureCounts.precheckRowCount,
  353. precheckOpenCount: proofGapClosureCounts.precheckOpenCount,
  354. precheckClosedCount: proofGapClosureCounts.precheckClosedCount,
  355. countSemantics: proofGapClosureCounts.countSemantics
  356. },
  357. proofGapOperatorPack: {
  358. title: '提号补证操作包',
  359. usage: '用于把 5 个真实证明缺口转成负责人、填写文件、待补字段、复验命令和通过标准;只安排补证,不证明客户效果。',
  360. directCustomerProof: false,
  361. proofLevel: proofGapOperatorPackSummary?.proofLevel || 'not_business_proof',
  362. exists: Boolean(proofGapOperatorPack.markdown || proofGapOperatorPack.csv),
  363. csv: proofGapOperatorPack.csv,
  364. markdown: proofGapOperatorPack.markdown,
  365. summary: proofGapOperatorPack.summary,
  366. generatedAt: proofGapOperatorPackSummary?.generatedAt || null,
  367. passed: Boolean(proofGapOperatorPackSummary?.passed),
  368. complete: Boolean(proofGapOperatorPackSummary?.complete),
  369. rowCount: Number(proofGapOperatorPackSummary?.rowCount || 0),
  370. openCount: Number(proofGapOperatorPackSummary?.openCount || 0),
  371. gapIds: Array.isArray(proofGapOperatorPackSummary?.gapIds)
  372. ? proofGapOperatorPackSummary.gapIds
  373. : (Array.isArray(proofGapOperatorPackSummary?.rows) ? proofGapOperatorPackSummary.rows.map(row => row.gapId).filter(Boolean) : []),
  374. fillFileCount: Number(proofGapOperatorPackSummary?.fillFileCount || 0),
  375. recheckCommandCount: Number(proofGapOperatorPackSummary?.recheckCommandCount || 0),
  376. ownerGroupCount: Number(proofGapOperatorPackSummary?.ownerGroupCount || (Array.isArray(proofGapOperatorPackSummary?.ownerGroups) ? proofGapOperatorPackSummary.ownerGroups.length : 0)),
  377. ownerArtifactCount: Number(proofGapOperatorPackSummary?.ownerArtifactCount || proofGapOperatorOwnerArtifacts.length || 0),
  378. ownerArtifacts: proofGapOperatorOwnerArtifacts,
  379. evidenceRowCount: Number(proofGapOperatorPackSummary?.evidenceRowCount || 0),
  380. nextActionCount: Number(proofGapOperatorPackSummary?.nextActionCount || 0),
  381. missingProofRequirementCount: Number(proofGapOperatorPackSummary?.missingProofRequirementCount || 0)
  382. },
  383. realProofIntakeBundle: {
  384. title: '真实验收材料收集总包',
  385. usage: '用于把真实材料模板、字段缺口、修复清单、证明缺口和复验命令集中成一个商务可打开的入口;不等同于客户效果证明。',
  386. directCustomerProof: false,
  387. proofLevel: realProofIntakeBundleSummary?.proofLevel || 'not_business_proof',
  388. exists: Boolean(realProofIntakeBundle.markdown || realProofIntakeBundle.csv),
  389. csv: realProofIntakeBundle.csv,
  390. markdown: realProofIntakeBundle.markdown,
  391. summary: realProofIntakeBundle.summary,
  392. generatedAt: realProofIntakeBundleSummary?.generatedAt || null,
  393. passed: Boolean(realProofIntakeBundleSummary?.passed),
  394. complete: Boolean(realProofIntakeBundleSummary?.complete),
  395. proofOpenCount: Number(realProofIntakeBundleSummary?.proofOpenCount || 0),
  396. fillRowCount: Number(realProofIntakeBundleSummary?.fillRowCount || 0),
  397. proofGapRowCount: Number(realProofIntakeBundleSummary?.proofGapRowCount || 0),
  398. recheckCommandCount: Number(realProofIntakeBundleSummary?.recheckCommandCount || 0),
  399. exportRowCount: Number(realProofIntakeBundleSummary?.exportRowCount || 0),
  400. operatorPackEvidenceRowCount: Number(realProofIntakeBundleSummary?.operatorPackEvidenceRowCount || realProofIntakeBundleSummary?.sourceState?.operatorPackEvidenceRowCount || 0),
  401. operatorPackNextActionCount: Number(realProofIntakeBundleSummary?.operatorPackNextActionCount || realProofIntakeBundleSummary?.sourceState?.operatorPackNextActionCount || 0),
  402. operatorPackMissingProofRequirementCount: Number(realProofIntakeBundleSummary?.operatorPackMissingProofRequirementCount || realProofIntakeBundleSummary?.sourceState?.operatorPackMissingProofRequirementCount || 0),
  403. operatorPackOwnerArtifactCount: Number(realProofIntakeBundleSummary?.operatorPackOwnerArtifactCount || realProofIntakeBundleSummary?.sourceState?.operatorPackOwnerArtifactCount || realProofOperatorPackOwnerArtifacts.length || 0),
  404. operatorPackOwnerArtifacts: realProofOperatorPackOwnerArtifacts
  405. },
  406. realProofClosureWorkOrder: {
  407. title: '真实证明闭环工单',
  408. usage: '用于把 intake/video/proof-gap/completion 的未关闭项拆成负责人、目标模板、字段、行范围、种子线索和复验命令;不写入正式模板,不等同于客户效果证明。',
  409. directCustomerProof: false,
  410. proofLevel: realProofClosureWorkOrderSummary?.proofLevel || 'not_business_proof',
  411. materialType: realProofClosureWorkOrderSummary?.materialType || 'real_proof_closure_work_order',
  412. exists: Boolean(realProofClosureWorkOrder.markdown || realProofClosureWorkOrder.csv),
  413. csv: realProofClosureWorkOrder.csv,
  414. markdown: realProofClosureWorkOrder.markdown,
  415. summary: realProofClosureWorkOrder.summary,
  416. generatedAt: realProofClosureWorkOrderSummary?.generatedAt || null,
  417. passed: Boolean(realProofClosureWorkOrderSummary?.passed),
  418. complete: Boolean(realProofClosureWorkOrderSummary?.complete),
  419. canCloseProofGap: Boolean(realProofClosureWorkOrderSummary?.canCloseProofGap),
  420. workOrderCount: Number(realProofClosureWorkOrderSummary?.workOrderCount || 0),
  421. ownerGroupCount: Number(realProofClosureWorkOrderSummary?.ownerGroupCount || 0),
  422. seedSuggestionCount: Number(realProofClosureWorkOrderSummary?.seedSuggestionCount || 0),
  423. proofGapOpenCount: Number(realProofClosureWorkOrderSummary?.sourceState?.proofGapOpenCount || 0),
  424. intakeFailureCount: Number(realProofClosureWorkOrderSummary?.sourceState?.intakeFailureCount || 0),
  425. videoFailureCount: Number(realProofClosureWorkOrderSummary?.sourceState?.videoFailureCount || 0),
  426. realCandidateRows: Number(realProofClosureWorkOrderSummary?.sourceState?.realCandidateRows || 0),
  427. ownerArtifacts: normalizeWorkOrderOwnerArtifacts(root, realProofClosureWorkOrderSummary?.ownerArtifacts || [])
  428. },
  429. localSeedToIntakeWorklist: {
  430. title: '本地种子转 intake 补表 worklist',
  431. usage: '用于把本地 DHA Brief、PoC 候选和参考账号解析转成可转写的历史数据草稿、视频资源补表清单和候选复核清单;不写入正式 intake 模板,不等同于客户效果证明。',
  432. directCustomerProof: false,
  433. proofLevel: localSeedToIntakeWorklistSummary?.proofLevel || 'not_business_proof',
  434. materialType: localSeedToIntakeWorklistSummary?.materialType || '',
  435. exists: Boolean(localSeedToIntakeWorklist.markdown || localSeedToIntakeWorklist.csv),
  436. markdown: localSeedToIntakeWorklist.markdown,
  437. csv: localSeedToIntakeWorklist.csv,
  438. summary: localSeedToIntakeWorklist.summary,
  439. historyDraft: localSeedToIntakeWorklist.historyDraft,
  440. videoWorklist: localSeedToIntakeWorklist.videoWorklist,
  441. candidateWorklist: localSeedToIntakeWorklist.candidateWorklist,
  442. generatedAt: localSeedToIntakeWorklistSummary?.generatedAt || null,
  443. complete: Boolean(localSeedToIntakeWorklistSummary?.complete),
  444. canCloseProofGap: Boolean(localSeedToIntakeWorklistSummary?.canCloseProofGap),
  445. historyDraftRows: Number(localSeedToIntakeWorklistSummary?.counts?.historyDraftRows || 0),
  446. referenceSeedRows: Number(localSeedToIntakeWorklistSummary?.counts?.referenceSeedRows || 0),
  447. candidateSeedRows: Number(localSeedToIntakeWorklistSummary?.counts?.candidateSeedRows || 0),
  448. videoWorklistRows: Number(localSeedToIntakeWorklistSummary?.counts?.videoWorklistRows || 0),
  449. videoCandidateNeedsResourceRows: Number(localSeedToIntakeWorklistSummary?.counts?.videoCandidateNeedsResourceRows || 0),
  450. missingProofRequirementCount: Array.isArray(localSeedToIntakeWorklistSummary?.missingProofRequirements) ? localSeedToIntakeWorklistSummary.missingProofRequirements.length : 0,
  451. targetFiles: Array.isArray(localSeedToIntakeWorklistSummary?.targetFiles) ? localSeedToIntakeWorklistSummary.targetFiles : [],
  452. nextActionCount: Array.isArray(localSeedToIntakeWorklistSummary?.nextActions) ? localSeedToIntakeWorklistSummary.nextActions.length : 0
  453. },
  454. realGapMaterialAudit: {
  455. title: 'three real gap material audit',
  456. usage: 'Shows that project materials exist for the three still-open proof gaps; it is not customer-effect, video A/B, or manual-supplement proof.',
  457. directCustomerProof: false,
  458. proofLevel: realGapMaterialAuditSummary?.proofLevel || 'not_business_proof',
  459. materialType: realGapMaterialAuditSummary?.materialType || 'three_real_gap_material_audit',
  460. exists: Boolean(realGapMaterialAudit.markdown || realGapMaterialAudit.csv),
  461. markdown: realGapMaterialAudit.markdown,
  462. csv: realGapMaterialAudit.csv,
  463. summary: realGapMaterialAudit.summary,
  464. generatedAt: realGapMaterialAuditSummary?.generatedAt || null,
  465. complete: Boolean(realGapMaterialAuditSummary?.complete),
  466. canCloseProofGap: Boolean(realGapMaterialAuditSummary?.canCloseProofGap),
  467. gapCount: Number(realGapMaterialAuditSummary?.gapCount || 0),
  468. materialFoundCount: Number(realGapMaterialAuditSummary?.materialFoundCount || 0),
  469. openGapCount: Number(realGapMaterialAuditSummary?.openGapCount || 0),
  470. sourceState: realGapMaterialAuditSummary?.sourceState || {},
  471. gapIds: Array.isArray(realGapMaterialAuditSummary?.gaps) ? realGapMaterialAuditSummary.gaps.map(item => item.id).filter(Boolean) : []
  472. },
  473. optimizationCompletion: {
  474. title: '优化完成度审计',
  475. usage: '用于汇总长期优化是否具备完成声明条件;readyForClaim=false 时不得宣称提号率提升、客户效果达标或人工补号量下降。',
  476. directCustomerProof: false,
  477. proofLevel: optimizationCompletionSummary?.readyForClaim ? 'real_evidence' : 'not_business_proof',
  478. exists: Boolean(optimizationCompletion.markdown || optimizationCompletion.summary),
  479. markdown: optimizationCompletion.markdown,
  480. summary: optimizationCompletion.summary,
  481. generatedAt: optimizationCompletionSummary?.generatedAt || null,
  482. complete: Boolean(optimizationCompletionSummary?.complete),
  483. readyForClaim: Boolean(optimizationCompletionSummary?.readyForClaim),
  484. proofGapOpenCount: Number(optimizationCompletionSummary?.proofGapOpenCount || 0),
  485. blockingReasonCount: Number(optimizationCompletionSummary?.blockingReasonCount ?? (Array.isArray(optimizationCompletionSummary?.blockingReasons) ? optimizationCompletionSummary.blockingReasons.length : 0)),
  486. blockingReasonsWithMissingProofRequirements: Number(optimizationCompletionSummary?.blockingReasonsWithMissingProofRequirements ?? countRowsWithMissingProofRequirements(optimizationCompletionSummary?.blockingReasons)),
  487. missingProofRequirementCount: Number(optimizationCompletionSummary?.missingProofRequirementCount ?? uniqueMissingProofRequirementCount(optimizationCompletionSummary?.blockingReasons)),
  488. videoAbPreflightReadyForVideoAb: Boolean(optimizationCompletionSummary?.videoAbPreflightReadyForVideoAb),
  489. videoAbPreflightFailureCount: Number.isFinite(Number(optimizationCompletionSummary?.videoAbPreflightFailureCount)) ? Number(optimizationCompletionSummary.videoAbPreflightFailureCount) : null,
  490. canClaim: {
  491. tihaoRateImproved: Boolean(optimizationCompletionSummary?.canClaim?.tihaoRateImproved),
  492. customerEffectAchieved: Boolean(optimizationCompletionSummary?.canClaim?.customerEffectAchieved),
  493. manualSupplementReduced: Boolean(optimizationCompletionSummary?.canClaim?.manualSupplementReduced)
  494. }
  495. },
  496. longRunReadiness: {
  497. title: '长跑前置门禁',
  498. usage: '用于判断 full-matrix/live 长跑是否可启动;ready=false 时只能继续补真实材料,不证明客户效果。',
  499. directCustomerProof: false,
  500. proofLevel: longRunReadinessSummary?.ready ? 'smoke_or_local' : 'not_business_proof',
  501. exists: Boolean(longRunReadiness.report || longRunReadiness.summary),
  502. report: longRunReadiness.report,
  503. summary: longRunReadiness.summary,
  504. generatedAt: longRunReadinessSummary?.generatedAt || null,
  505. mode: longRunReadinessSummary?.mode || null,
  506. ready: Boolean(longRunReadinessSummary?.ready),
  507. passCount: Number(longRunReadinessSummary?.counts?.pass || 0),
  508. failCount: Number(longRunReadinessSummary?.counts?.fail || 0),
  509. warnCount: Number(longRunReadinessSummary?.counts?.warn || 0),
  510. nextCommandCount: Array.isArray(longRunReadinessSummary?.nextCommands) ? longRunReadinessSummary.nextCommands.length : 0,
  511. boundaryCount: Array.isArray(longRunReadinessSummary?.boundaries) ? longRunReadinessSummary.boundaries.length : 0
  512. },
  513. nextActions: {
  514. complete: Boolean(nextActionsSummary?.complete),
  515. actionCount,
  516. proofOpenCount,
  517. ownerGroupCount: Array.isArray(nextActionsSummary?.ownerGroups) ? nextActionsSummary.ownerGroups.length : 0,
  518. ownerArtifactCount: ownerArtifacts.length,
  519. sourceState: nextActionSourceState,
  520. topActions: nextActionTopActions,
  521. ownerGroups: nextActionsSummary?.ownerGroups || [],
  522. ownerArtifacts
  523. },
  524. supportDocs,
  525. repairActions: {
  526. dataIntake: {
  527. title: '历史数据/商务复核修复清单',
  528. csv: relIfExists(root, path.join(intakeReadinessDir, 'intake-readiness-repair-actions.csv')),
  529. report: relIfExists(root, path.join(intakeReadinessDir, 'intake-readiness-report.md')),
  530. summary: relIfExists(root, path.join(intakeReadinessDir, 'intake-readiness-summary.json')),
  531. failureCount: Number(intakeReadinessSummary?.failureCount || 0),
  532. repairActionCount: Number(intakeReadinessSummary?.repairActions?.length || 0),
  533. ready: Boolean(intakeReadinessSummary?.acceptance?.overallReady)
  534. },
  535. videoResource: {
  536. title: '视频资源修复清单',
  537. csv: relIfExists(root, path.join(videoResourceReadinessDir, 'video-resource-repair-actions.csv')),
  538. report: relIfExists(root, path.join(videoResourceReadinessDir, 'video-resource-readiness-report.md')),
  539. summary: relIfExists(root, path.join(videoResourceReadinessDir, 'video-resource-readiness-summary.json')),
  540. failureCount: Number(videoResourceReadinessSummary?.failureCount || 0),
  541. repairActionCount: Number(videoResourceReadinessSummary?.repairActions?.length || 0),
  542. readyForVideoAbPreflight: Boolean(videoResourceReadinessSummary?.acceptance?.readyForVideoAbPreflight)
  543. },
  544. homepageEvidence: {
  545. title: '主页近期内容证据修复清单',
  546. csv: relIfExists(root, path.join(homepageEvidenceReadinessDir, 'homepage-evidence-repair-actions.csv')),
  547. report: relIfExists(root, path.join(homepageEvidenceReadinessDir, 'homepage-evidence-readiness-report.md')),
  548. summary: relIfExists(root, path.join(homepageEvidenceReadinessDir, 'homepage-evidence-readiness-summary.json')),
  549. failureCount: Number(homepageEvidenceReadinessSummary?.failureCount || 0),
  550. repairActionCount: Number(homepageEvidenceReadinessSummary?.repairActions?.length || 0),
  551. ready: Boolean(homepageEvidenceReadinessSummary?.ready),
  552. directCustomerProof: Boolean(homepageEvidenceReadinessSummary?.directCustomerProof)
  553. }
  554. },
  555. handoff: {
  556. complete: Boolean(handoffSummary?.complete),
  557. summary: relIfExists(root, path.join(handoffDir, 'optimization-handoff-summary.json')),
  558. report: relIfExists(root, path.join(handoffDir, 'optimization-handoff-report.md'))
  559. },
  560. guardrails: [
  561. '最新提号补证表单只安排补证任务,不证明客户效果完成。',
  562. '最新软件端候选名单需要商务复核和客户反馈后才能进入客户效果审计。',
  563. '不能把 sample、smoke、模板、provider fallback 或接口 200 当成业务证明。',
  564. '本索引不得包含 sessionToken、鉴权头、模型 token 或 npm token。'
  565. ],
  566. recommendedNextCommand: 'npm run round:refresh -- --output-root outputs --strict'
  567. };
  568. }
  569. function renderReport(summary) {
  570. const lines = [
  571. '# 最新提号表单索引',
  572. '',
  573. `- 生成时间:${summary.generatedAt}`,
  574. `- 是否通过:${summary.passed ? '是' : '否'}`,
  575. `- 用途:${summary.purpose}`,
  576. '',
  577. '## 当前可用文件',
  578. '',
  579. '| 类型 | 路径 | 状态 | 用法 |',
  580. '| --- | --- | --- | --- |',
  581. tableRow([
  582. summary.primaryForm.title,
  583. summary.primaryForm.csv || '缺失',
  584. summary.primaryForm.passed ? `通过,${summary.primaryForm.rowCount} 行,无重复 ID` : '未通过或缺失',
  585. summary.primaryForm.usage
  586. ]),
  587. tableRow([
  588. summary.clientList.title,
  589. summary.clientList.csv || '缺失',
  590. summary.clientList.exists
  591. ? `存在,${summary.clientList.rowCount || '行数待摘要确认'} 行;最终重复组 ${summary.clientList.finalDuplicateGroupCount ?? 'unknown'};排名连续 ${summary.clientList.rankContinuous ? '是' : '待确认'};源表去重丢弃 ${summary.clientList.sourceDuplicateRemovedCount ?? 0} 行`
  592. : '缺失',
  593. summary.clientList.usage
  594. ]),
  595. tableRow([
  596. summary.businessExecutionIndex.title,
  597. summary.businessExecutionIndex.markdown || summary.businessExecutionIndex.csv || '缺失',
  598. summary.businessExecutionIndex.exists
  599. ? `通过=${summary.businessExecutionIndex.passed},${summary.businessExecutionIndex.rowCount} 行,操作包负责人附件 ${summary.businessExecutionIndex.operatorPackOwnerArtifactCount},字段负责人 ${summary.businessExecutionIndex.fieldOwnerRowCount},复验命令 ${summary.businessExecutionIndex.recheckCommandRowCount}`
  600. : '缺失',
  601. summary.businessExecutionIndex.usage
  602. ]),
  603. tableRow([
  604. summary.planExecutionStatus.title,
  605. summary.planExecutionStatus.markdown || summary.planExecutionStatus.csv || '缺失',
  606. summary.planExecutionStatus.exists
  607. ? `complete=${summary.planExecutionStatus.complete},rowCount=${summary.planExecutionStatus.rowCount},proofOpenCount=${summary.planExecutionStatus.proofOpenCount},operatorPackOwnerArtifactCount=${summary.planExecutionStatus.operatorPackOwnerArtifactCount}`
  608. : '缺失',
  609. summary.planExecutionStatus.usage
  610. ]),
  611. tableRow([
  612. summary.proofGapRequest.title,
  613. summary.proofGapRequest.markdown || summary.proofGapRequest.csv || '缺失',
  614. summary.proofGapRequest.exists
  615. ? `complete=${summary.proofGapRequest.complete},unresolved=${summary.proofGapRequest.unresolvedCount},rows=${summary.proofGapRequest.rowCount}`
  616. : '缺失',
  617. summary.proofGapRequest.usage
  618. ]),
  619. tableRow([
  620. summary.proofGapClosure.title,
  621. summary.proofGapClosure.markdown || summary.proofGapClosure.summary || '缺失',
  622. summary.proofGapClosure.exists
  623. ? `complete=${summary.proofGapClosure.complete},openCount=${summary.proofGapClosure.openCount},businessOpenCount=${summary.proofGapClosure.businessOpenCount},precheckOpenCount=${summary.proofGapClosure.precheckOpenCount},totalRows=${summary.proofGapClosure.totalRows}`
  624. : '缺失',
  625. summary.proofGapClosure.usage
  626. ]),
  627. tableRow([
  628. summary.proofGapOperatorPack.title,
  629. summary.proofGapOperatorPack.markdown || summary.proofGapOperatorPack.csv || '缺失',
  630. summary.proofGapOperatorPack.exists
  631. ? `passed=${summary.proofGapOperatorPack.passed},openCount=${summary.proofGapOperatorPack.openCount},rowCount=${summary.proofGapOperatorPack.rowCount},ownerGroupCount=${summary.proofGapOperatorPack.ownerGroupCount},ownerArtifactCount=${summary.proofGapOperatorPack.ownerArtifactCount},recheckCommandCount=${summary.proofGapOperatorPack.recheckCommandCount},evidenceRowCount=${summary.proofGapOperatorPack.evidenceRowCount},nextActionCount=${summary.proofGapOperatorPack.nextActionCount},gapIds=${summary.proofGapOperatorPack.gapIds.join(';')}`
  632. : '缺失',
  633. summary.proofGapOperatorPack.usage
  634. ]),
  635. tableRow([
  636. summary.realProofIntakeBundle.title,
  637. summary.realProofIntakeBundle.markdown || summary.realProofIntakeBundle.csv || '缺失',
  638. summary.realProofIntakeBundle.exists
  639. ? `passed=${summary.realProofIntakeBundle.passed},proofOpenCount=${summary.realProofIntakeBundle.proofOpenCount},填表 ${summary.realProofIntakeBundle.fillRowCount},复验命令 ${summary.realProofIntakeBundle.recheckCommandCount},operatorPackEvidenceRowCount=${summary.realProofIntakeBundle.operatorPackEvidenceRowCount},operatorPackNextActionCount=${summary.realProofIntakeBundle.operatorPackNextActionCount},operatorPackOwnerArtifactCount=${summary.realProofIntakeBundle.operatorPackOwnerArtifactCount}`
  640. : '缺失',
  641. summary.realProofIntakeBundle.usage
  642. ]),
  643. tableRow([
  644. summary.realProofClosureWorkOrder.title,
  645. summary.realProofClosureWorkOrder.markdown || summary.realProofClosureWorkOrder.csv || '缺失',
  646. summary.realProofClosureWorkOrder.exists
  647. ? `passed=${summary.realProofClosureWorkOrder.passed},complete=${summary.realProofClosureWorkOrder.complete},workOrderCount=${summary.realProofClosureWorkOrder.workOrderCount},ownerGroupCount=${summary.realProofClosureWorkOrder.ownerGroupCount},proofGapOpenCount=${summary.realProofClosureWorkOrder.proofGapOpenCount},intakeFailureCount=${summary.realProofClosureWorkOrder.intakeFailureCount},realCandidateRows=${summary.realProofClosureWorkOrder.realCandidateRows},canCloseProofGap=${summary.realProofClosureWorkOrder.canCloseProofGap}`
  648. : '缺失',
  649. summary.realProofClosureWorkOrder.usage
  650. ]),
  651. tableRow([
  652. summary.localSeedToIntakeWorklist.title,
  653. summary.localSeedToIntakeWorklist.markdown || summary.localSeedToIntakeWorklist.csv || '缺失',
  654. summary.localSeedToIntakeWorklist.exists
  655. ? `proofLevel=${summary.localSeedToIntakeWorklist.proofLevel},historyDraftRows=${summary.localSeedToIntakeWorklist.historyDraftRows},candidateSeedRows=${summary.localSeedToIntakeWorklist.candidateSeedRows},videoWorklistRows=${summary.localSeedToIntakeWorklist.videoWorklistRows},videoCandidateNeedsResourceRows=${summary.localSeedToIntakeWorklist.videoCandidateNeedsResourceRows}`
  656. : '缺失',
  657. summary.localSeedToIntakeWorklist.usage
  658. ]),
  659. tableRow([
  660. summary.realGapMaterialAudit.title,
  661. summary.realGapMaterialAudit.markdown || summary.realGapMaterialAudit.csv || 'missing',
  662. summary.realGapMaterialAudit.exists
  663. ? `proofLevel=${summary.realGapMaterialAudit.proofLevel}, materialFound=${summary.realGapMaterialAudit.materialFoundCount}/${summary.realGapMaterialAudit.gapCount}, openGapCount=${summary.realGapMaterialAudit.openGapCount}, canCloseProofGap=${summary.realGapMaterialAudit.canCloseProofGap}`
  664. : 'missing',
  665. summary.realGapMaterialAudit.usage
  666. ]),
  667. tableRow([
  668. summary.optimizationCompletion.title,
  669. summary.optimizationCompletion.markdown || summary.optimizationCompletion.summary || '缺失',
  670. summary.optimizationCompletion.exists
  671. ? `complete=${summary.optimizationCompletion.complete},readyForClaim=${summary.optimizationCompletion.readyForClaim},proofGapOpenCount=${summary.optimizationCompletion.proofGapOpenCount},blockingReasons=${summary.optimizationCompletion.blockingReasonCount},missingProofRequirementCount=${summary.optimizationCompletion.missingProofRequirementCount},videoAbPreflightReadyForVideoAb=${summary.optimizationCompletion.videoAbPreflightReadyForVideoAb},videoAbPreflightFailureCount=${summary.optimizationCompletion.videoAbPreflightFailureCount ?? 'unknown'}`
  672. : '缺失',
  673. summary.optimizationCompletion.usage
  674. ]),
  675. tableRow([
  676. summary.longRunReadiness.title,
  677. summary.longRunReadiness.report || summary.longRunReadiness.summary || '缺失',
  678. summary.longRunReadiness.exists
  679. ? `mode=${summary.longRunReadiness.mode || 'unknown'},ready=${summary.longRunReadiness.ready},pass=${summary.longRunReadiness.passCount},fail=${summary.longRunReadiness.failCount},warn=${summary.longRunReadiness.warnCount}`
  680. : '缺失',
  681. summary.longRunReadiness.usage
  682. ]),
  683. '',
  684. '## 下一步行动队列摘要',
  685. '',
  686. `- complete:${summary.nextActions.complete ? 'true' : 'false'}`,
  687. `- actionCount:${summary.nextActions.actionCount}`,
  688. `- proofOpenCount:${summary.nextActions.proofOpenCount}`,
  689. `- ownerGroupCount:${summary.nextActions.ownerGroupCount || 0}`,
  690. `- ownerArtifactCount:${summary.nextActions.ownerArtifactCount || 0}`,
  691. `- intakeFailureCount:${summary.nextActions.sourceState?.intakeFailureCount || 0}`,
  692. `- videoFailureCount:${summary.nextActions.sourceState?.videoFailureCount || 0}`,
  693. `- pipelineReadyForClaim:${summary.nextActions.sourceState?.pipelineReadyForClaim ? 'true' : 'false'}`,
  694. `- pipelineStale:${summary.nextActions.sourceState?.pipelineStale ? 'true' : 'false'}`,
  695. `- pipelineSuppressedNextActionCount:${summary.nextActions.sourceState?.pipelineSuppressedNextActionCount || 0}`,
  696. summary.nextActions.sourceState?.pipelineStaleReason ? `- pipelineStaleReason:${summary.nextActions.sourceState.pipelineStaleReason}` : '',
  697. '- 边界:该队列只用于补证派工和复验;proofOpenCount 不为 0 时不得宣称提号率、客户效果或人工补号量改善。',
  698. '',
  699. '| ID | 优先级 | 负责人 | 动作 | 状态 | 命令 | 通过标准 | 缺证明项 |',
  700. '| --- | ---: | --- | --- | --- | --- | --- | ---: |',
  701. ...summary.nextActions.topActions.map(item => tableRow([
  702. item.id || '缺失',
  703. item.priority || item.order || 0,
  704. item.owner || '缺失',
  705. item.title || '缺失',
  706. item.status || '缺失',
  707. item.command || '缺失',
  708. item.acceptance || '缺失',
  709. item.missingProofRequirementCount || 0
  710. ])),
  711. '',
  712. '## 负责人行动文件',
  713. '',
  714. '| 负责人 | 动作数 | Markdown | CSV | 修复附件 |',
  715. '| --- | ---: | --- | --- | --- |',
  716. ...summary.nextActions.ownerArtifacts.map(item => tableRow([
  717. item.owner,
  718. item.actionCount,
  719. item.markdown || '缺失',
  720. item.csv || '缺失',
  721. renderRepairArtifactSummary(item.repairArtifacts || [])
  722. ])),
  723. '',
  724. '## proof-gap 操作包负责人文件',
  725. '',
  726. '| 负责人 | 动作数 | Markdown | CSV | 缺口 | 当前证据行 | 下一步动作 |',
  727. '| --- | ---: | --- | --- | --- | ---: | ---: |',
  728. ...summary.proofGapOperatorPack.ownerArtifacts.map(item => tableRow([
  729. item.owner,
  730. item.actionCount,
  731. item.markdown || '缺失',
  732. item.csv || '缺失',
  733. (item.gapIds || []).join('; ') || '缺失',
  734. item.evidenceRowCount,
  735. item.nextActionCount
  736. ])),
  737. '',
  738. '## 商务执行总表操作包负责人附件',
  739. '',
  740. '| 负责人 | 动作数 | Markdown | CSV | 缺口 | 当前证据行 | 下一步动作 |',
  741. '| --- | ---: | --- | --- | --- | ---: | ---: |',
  742. ...summary.businessExecutionIndex.operatorPackOwnerArtifacts.map(item => tableRow([
  743. item.owner,
  744. item.actionCount,
  745. item.markdown || '缺失',
  746. item.csv || '缺失',
  747. (item.gapIds || []).join('; ') || '缺失',
  748. item.evidenceRowCount,
  749. item.nextActionCount
  750. ])),
  751. '',
  752. '## 真实验收总包操作包负责人附件',
  753. '',
  754. '| 负责人 | 动作数 | Markdown | CSV | 缺口 | 当前证据行 | 下一步动作 |',
  755. '| --- | ---: | --- | --- | --- | ---: | ---: |',
  756. ...summary.realProofIntakeBundle.operatorPackOwnerArtifacts.map(item => tableRow([
  757. item.owner,
  758. item.actionCount,
  759. item.markdown || '缺失',
  760. item.csv || '缺失',
  761. (item.gapIds || []).join('; ') || '缺失',
  762. item.evidenceRowCount,
  763. item.nextActionCount
  764. ])),
  765. '',
  766. '## 真实证明闭环工单负责人附件',
  767. '',
  768. '| 负责人 | 工单数 | Markdown | CSV | 缺口 | 目标文件 |',
  769. '| --- | ---: | --- | --- | --- | --- |',
  770. ...summary.realProofClosureWorkOrder.ownerArtifacts.map(item => tableRow([
  771. item.owner,
  772. item.workOrderCount,
  773. item.markdown || '缺失',
  774. item.csv || '缺失',
  775. (item.gapIds || []).join('; ') || '缺失',
  776. (item.targetFiles || []).join('; ') || '缺失'
  777. ])),
  778. '',
  779. '## 本地种子转 intake 补表 worklist',
  780. '',
  781. renderLocalSeedToIntakeWorklist(summary.localSeedToIntakeWorklist),
  782. '',
  783. '## three real gap material audit',
  784. '',
  785. renderRealGapMaterialAudit(summary.realGapMaterialAudit),
  786. '',
  787. '## 修复清单入口',
  788. '',
  789. '| 类型 | CSV | 当前问题数 | 修复动作数 | 状态 |',
  790. '| --- | --- | ---: | ---: | --- |',
  791. tableRow([
  792. summary.repairActions.dataIntake.title,
  793. summary.repairActions.dataIntake.csv || '缺失',
  794. summary.repairActions.dataIntake.failureCount,
  795. summary.repairActions.dataIntake.repairActionCount,
  796. summary.repairActions.dataIntake.ready ? '已就绪' : '待补齐'
  797. ]),
  798. tableRow([
  799. summary.repairActions.homepageEvidence.title,
  800. summary.repairActions.homepageEvidence.csv || '缺失',
  801. summary.repairActions.homepageEvidence.failureCount,
  802. summary.repairActions.homepageEvidence.repairActionCount,
  803. summary.repairActions.homepageEvidence.ready ? '主页证据已就绪' : '待补主页近期内容证据'
  804. ]),
  805. tableRow([
  806. summary.repairActions.videoResource.title,
  807. summary.repairActions.videoResource.csv || '缺失',
  808. summary.repairActions.videoResource.failureCount,
  809. summary.repairActions.videoResource.repairActionCount,
  810. summary.repairActions.videoResource.readyForVideoAbPreflight ? '可进入视频 A/B 前置' : '待补真实视频资源'
  811. ]),
  812. '',
  813. '## 当前边界',
  814. '',
  815. `- 补证行动数:${summary.nextActions.actionCount}`,
  816. `- 未关闭证明缺口:${summary.nextActions.proofOpenCount}`,
  817. `- proofGapClosure.businessOpenCount:${summary.proofGapClosure.businessOpenCount}`,
  818. `- proofGapClosure.precheckOpenCount:${summary.proofGapClosure.precheckOpenCount}`,
  819. `- optimizationCompletion.videoAbPreflightReadyForVideoAb:${summary.optimizationCompletion.videoAbPreflightReadyForVideoAb ? 'true' : 'false'}`,
  820. `- optimizationCompletion.videoAbPreflightFailureCount:${summary.optimizationCompletion.videoAbPreflightFailureCount ?? 'unknown'}`,
  821. `- 交接摘要 complete:${summary.handoff.complete}`,
  822. '',
  823. '## 下一步命令',
  824. '',
  825. '```powershell',
  826. summary.recommendedNextCommand,
  827. '```',
  828. '',
  829. '## 防误用边界',
  830. '',
  831. ...summary.guardrails.map(item => `- ${item}`)
  832. ];
  833. const supportDocLines = [
  834. '## 中文 SOP 入口',
  835. '',
  836. '| 文档 | 路径 | 用法 |',
  837. '| --- | --- | --- |',
  838. ...summary.supportDocs.map(item => tableRow([
  839. item.title,
  840. item.path || '缺失',
  841. item.usage
  842. ])),
  843. ''
  844. ];
  845. const insertAt = lines.findIndex((line, index) => index > 10 && String(line).startsWith('## '));
  846. const currentFilesEnd = lines.findIndex((line, index) => index > 10 && line === '' && String(lines[index + 1] || '').startsWith('## '));
  847. if (currentFilesEnd > 0) {
  848. lines.splice(currentFilesEnd, 0, tableRow([
  849. summary.intakeFieldChecklist.title,
  850. summary.intakeFieldChecklist.markdown || summary.intakeFieldChecklist.csv || '缺失',
  851. summary.intakeFieldChecklist.exists ? `存在,${summary.intakeFieldChecklist.itemCount} 项,缺必填 ${summary.intakeFieldChecklist.missingRequiredCount},占位 ${summary.intakeFieldChecklist.placeholderCount}` : '缺失',
  852. summary.intakeFieldChecklist.usage
  853. ]), tableRow([
  854. summary.manualReviewLabelGuide.title,
  855. summary.manualReviewLabelGuide.markdown || summary.manualReviewLabelGuide.csv || '缺失',
  856. summary.manualReviewLabelGuide.exists
  857. ? `通过=${summary.manualReviewLabelGuide.passed},标签 ${summary.manualReviewLabelGuide.labelCount},归因 ${summary.manualReviewLabelGuide.attributionTypeCount}`
  858. : '缺失',
  859. summary.manualReviewLabelGuide.usage
  860. ]), '');
  861. }
  862. if (summary.intakeFieldChecklist.ownerGroups.length) {
  863. lines.splice(currentFilesEnd + 2, 0, ...renderIntakeOwnerGroups(summary.intakeFieldChecklist.ownerGroups));
  864. }
  865. if (summary.businessFillGuide?.rows?.length) {
  866. const ownerGroupInsertAt = lines.findIndex(line => line === '## 负责人行动文件');
  867. lines.splice(ownerGroupInsertAt > 0 ? ownerGroupInsertAt : currentFilesEnd + 2, 0, ...renderBusinessFillGuide(summary.businessFillGuide));
  868. }
  869. if (summary.intakeFieldChecklist.recheckCommands.length) {
  870. const ownerGroupInsertAt = lines.findIndex(line => line === '## 负责人行动文件');
  871. lines.splice(ownerGroupInsertAt > 0 ? ownerGroupInsertAt : currentFilesEnd + 2, 0, ...renderRecheckCommands(summary.intakeFieldChecklist.recheckCommands));
  872. }
  873. lines.splice(insertAt > 0 ? insertAt : 0, 0, ...supportDocLines);
  874. return lines.join('\n');
  875. }
  876. function renderLocalSeedToIntakeWorklist(worklist) {
  877. if (!worklist || !worklist.exists) {
  878. return '- 尚未找到本地种子转 intake 补表 worklist,请运行 `npm run local-seed-to-intake:worklist -- --outputs outputs --output outputs\\local-seed-to-intake-worklist-latest`。';
  879. }
  880. return [
  881. `- 报告:${worklist.markdown || '缺失'}`,
  882. `- 汇总 CSV:${worklist.csv || '缺失'}`,
  883. `- Summary:${worklist.summary || '缺失'}`,
  884. `- history-intake-draft:${worklist.historyDraft || '缺失'}`,
  885. `- video-resource-worklist:${worklist.videoWorklist || '缺失'}`,
  886. `- candidate-seed-worklist:${worklist.candidateWorklist || '缺失'}`,
  887. `- proofLevel:${worklist.proofLevel || 'not_business_proof'}`,
  888. `- materialType:${worklist.materialType || 'seed_to_intake_worklist'}`,
  889. `- directCustomerProof:${worklist.directCustomerProof ? 'true' : 'false'}`,
  890. `- canCloseProofGap:${worklist.canCloseProofGap ? 'true' : 'false'}`,
  891. `- historyDraftRows:${worklist.historyDraftRows || 0}`,
  892. `- referenceSeedRows:${worklist.referenceSeedRows || 0}`,
  893. `- candidateSeedRows:${worklist.candidateSeedRows || 0}`,
  894. `- videoWorklistRows:${worklist.videoWorklistRows || 0}`,
  895. `- videoCandidateNeedsResourceRows:${worklist.videoCandidateNeedsResourceRows || 0}`,
  896. `- missingProofRequirementCount:${worklist.missingProofRequirementCount || 0}`,
  897. `- nextActionCount:${worklist.nextActionCount || 0}`,
  898. `- 目标模板:${(worklist.targetFiles || []).join(';') || '缺失'}`,
  899. '- 边界:该 worklist 只把本地种子转成补表草稿;空字段仍需商务/投放补真实材料,不能关闭 proof-gap。'
  900. ].join('\n');
  901. }
  902. function renderRealGapMaterialAudit(audit) {
  903. if (!audit || !audit.exists) {
  904. return '- missing; run `npm run real-gap:materials -- --outputs outputs --output outputs\\real-gap-material-audit-latest --strict`.';
  905. }
  906. return [
  907. `- report: ${audit.markdown || 'missing'}`,
  908. `- csv: ${audit.csv || 'missing'}`,
  909. `- summary: ${audit.summary || 'missing'}`,
  910. `- proofLevel: ${audit.proofLevel || 'not_business_proof'}`,
  911. `- materialType: ${audit.materialType || 'three_real_gap_material_audit'}`,
  912. `- directCustomerProof: ${audit.directCustomerProof ? 'true' : 'false'}`,
  913. `- canCloseProofGap: ${audit.canCloseProofGap ? 'true' : 'false'}`,
  914. `- complete: ${audit.complete ? 'true' : 'false'}`,
  915. `- materialFound: ${audit.materialFoundCount || 0}/${audit.gapCount || 0}`,
  916. `- openGapCount: ${audit.openGapCount || 0}`,
  917. `- gapIds: ${(audit.gapIds || []).join('; ') || 'missing'}`,
  918. '- boundary: material coverage does not prove tihao-rate improvement, customer effect, video A/B lift, or manual supplement reduction.'
  919. ].join('\n');
  920. }
  921. function renderIntakeOwnerGroups(ownerGroups) {
  922. return [
  923. '## 字段补齐负责人顺序',
  924. '',
  925. '| 负责人 | 范围 | 字段项 | 缺必填 | 占位 | 文件 | 下一步 | 通过标准 | 边界 |',
  926. '| --- | --- | ---: | ---: | ---: | --- | --- | --- | --- |',
  927. ...ownerGroups.map(item => tableRow([
  928. item.owner,
  929. item.section,
  930. item.itemCount,
  931. item.missingRequiredCount,
  932. item.placeholderCount,
  933. (item.files || []).join(';') || '缺失',
  934. item.nextAction,
  935. item.acceptance,
  936. item.boundary
  937. ])),
  938. ''
  939. ];
  940. }
  941. function renderRecheckCommands(recheckCommands) {
  942. return [
  943. '## 补齐后复验命令',
  944. '',
  945. '| 阶段 | 负责人 | 命令 | 通过标准 | 边界 |',
  946. '| --- | --- | --- | --- | --- |',
  947. ...recheckCommands.map(item => tableRow([
  948. item.stage,
  949. item.owner,
  950. item.command,
  951. item.acceptance,
  952. item.boundary
  953. ])),
  954. ''
  955. ];
  956. }
  957. function buildBusinessFillGuide({ ownerGroups, recheckCommands }) {
  958. const rows = (ownerGroups || []).map((group, index) => {
  959. const recheck = recheckForSection(group.section, recheckCommands);
  960. return {
  961. order: index + 1,
  962. owner: group.owner || '',
  963. section: group.section || '',
  964. file: (group.files || [])[0] || '',
  965. firstItemIds: group.firstItemIds || [],
  966. issueCount: Number(group.itemCount || 0),
  967. missingRequiredCount: Number(group.missingRequiredCount || 0),
  968. placeholderCount: Number(group.placeholderCount || 0),
  969. fillAction: group.nextAction || '',
  970. recheckCommand: recheck?.command || '',
  971. acceptance: group.acceptance || recheck?.acceptance || '',
  972. boundary: group.boundary || '补齐材料,不证明客户效果'
  973. };
  974. });
  975. return {
  976. title: '商务填表执行视图',
  977. directCustomerProof: false,
  978. proofLevel: 'smoke_or_local',
  979. rowCount: rows.length,
  980. rows,
  981. guardrail: '本视图只帮助商务按表补齐真实材料,不代表客户效果已达标。'
  982. };
  983. }
  984. function recheckForSection(section, recheckCommands) {
  985. const commands = Array.isArray(recheckCommands) ? recheckCommands : [];
  986. if (section === 'video') {
  987. return commands.find(item => /video:resource-readiness/.test(item.command)) ||
  988. commands.find(item => /acceptance:video-ab/.test(item.command)) ||
  989. null;
  990. }
  991. if (section === 'history' || section === 'review') {
  992. return commands.find(item => /intake:readiness/.test(item.command)) ||
  993. commands.find(item => /customer-effect:audit/.test(item.command)) ||
  994. null;
  995. }
  996. return commands[0] || null;
  997. }
  998. function renderBusinessFillGuide(guide) {
  999. return [
  1000. `## ${guide.title}`,
  1001. '',
  1002. '| 顺序 | 负责人 | 表单区域 | 填表文件 | 优先项 | 问题数 | 填表动作 | 补齐后复验 | 验收 | 边界 |',
  1003. '| ---: | --- | --- | --- | --- | ---: | --- | --- | --- | --- |',
  1004. ...guide.rows.map(item => tableRow([
  1005. item.order,
  1006. item.owner,
  1007. item.section,
  1008. item.file || '缺失',
  1009. (item.firstItemIds || []).join('、') || '无',
  1010. item.issueCount,
  1011. item.fillAction,
  1012. item.recheckCommand || '待补齐后运行 round:refresh',
  1013. item.acceptance,
  1014. item.boundary
  1015. ])),
  1016. '',
  1017. `- ${guide.guardrail}`,
  1018. ''
  1019. ];
  1020. }
  1021. function normalizeIntakeOwnerGroups(root, ownerGroups) {
  1022. if (!Array.isArray(ownerGroups)) return [];
  1023. return ownerGroups.map(item => ({
  1024. owner: item.owner || '',
  1025. section: item.section || '',
  1026. itemCount: Number(item.itemCount || 0),
  1027. missingRequiredCount: Number(item.missingRequiredCount || 0),
  1028. placeholderCount: Number(item.placeholderCount || 0),
  1029. files: Array.isArray(item.files) ? item.files.map(file => relIfPossible(root, file)) : [],
  1030. firstItemIds: Array.isArray(item.firstItemIds) ? item.firstItemIds : [],
  1031. nextAction: item.nextAction || '',
  1032. acceptance: item.acceptance || '',
  1033. boundary: item.boundary || ''
  1034. }));
  1035. }
  1036. function normalizeRecheckCommands(recheckCommands) {
  1037. if (!Array.isArray(recheckCommands)) return [];
  1038. return recheckCommands.map(item => ({
  1039. stage: item.stage || '',
  1040. owner: item.owner || '',
  1041. command: item.command || '',
  1042. acceptance: item.acceptance || '',
  1043. boundary: item.boundary || ''
  1044. }));
  1045. }
  1046. function normalizeProofGapClosureCounts(summary) {
  1047. const rows = Array.isArray(summary?.rows) ? summary.rows : [];
  1048. const precheckRows = rows.filter(row => row.id === 'intake-readiness');
  1049. const businessRows = rows.filter(row => row.id !== 'intake-readiness');
  1050. const fallbackBusinessOpen = businessRows.filter(row => row.status === 'open').length;
  1051. const fallbackBusinessClosed = businessRows.filter(row => row.status === 'closed').length;
  1052. const fallbackPrecheckOpen = precheckRows.filter(row => row.status === 'open').length;
  1053. const fallbackPrecheckClosed = precheckRows.filter(row => row.status === 'closed').length;
  1054. return {
  1055. businessOpenCount: numberOr(summary?.businessOpenCount, numberOr(summary?.openCount, fallbackBusinessOpen)),
  1056. businessClosedCount: numberOr(summary?.businessClosedCount, numberOr(summary?.closedCount, fallbackBusinessClosed)),
  1057. businessGapCount: numberOr(summary?.businessGapCount, businessRows.length),
  1058. totalRows: numberOr(summary?.totalRows, rows.length),
  1059. precheckRowCount: numberOr(summary?.precheckRowCount, precheckRows.length),
  1060. precheckOpenCount: numberOr(summary?.precheckOpenCount, fallbackPrecheckOpen),
  1061. precheckClosedCount: numberOr(summary?.precheckClosedCount, fallbackPrecheckClosed),
  1062. countSemantics: summary?.countSemantics || {
  1063. openCount: 'business_gap_open_count_excludes_precheck_rows',
  1064. closedCount: 'business_gap_closed_count_excludes_precheck_rows',
  1065. businessOpenCount: 'same_as_openCount',
  1066. businessClosedCount: 'same_as_closedCount',
  1067. precheckOpenCount: 'precheck_rows_reported_separately'
  1068. }
  1069. };
  1070. }
  1071. function numberOr(value, fallback) {
  1072. const number = Number(value);
  1073. return Number.isFinite(number) ? number : fallback;
  1074. }
  1075. function normalizeBusinessProofNextActionSourceState(sourceState) {
  1076. if (!sourceState || typeof sourceState !== 'object') return {};
  1077. return {
  1078. progressComplete: Boolean(sourceState.progressComplete),
  1079. progressCounts: sourceState.progressCounts || {},
  1080. proofGapOpenCount: Number(sourceState.proofGapOpenCount || 0),
  1081. intakeFailureCount: Number(sourceState.intakeFailureCount || 0),
  1082. videoFailureCount: Number(sourceState.videoFailureCount || 0),
  1083. homepageFailureCount: Number(sourceState.homepageFailureCount || 0),
  1084. pipelineReadyForClaim: Boolean(sourceState.pipelineReadyForClaim),
  1085. pipelineFailCount: Number(sourceState.pipelineFailCount || 0),
  1086. pipelineNextActionCount: Number(sourceState.pipelineNextActionCount || 0),
  1087. pipelineRawNextActionCount: Number(sourceState.pipelineRawNextActionCount || 0),
  1088. pipelineSuppressedNextActionCount: Number(sourceState.pipelineSuppressedNextActionCount || 0),
  1089. pipelineStale: Boolean(sourceState.pipelineStale),
  1090. pipelineStaleReason: sourceState.pipelineStaleReason || '',
  1091. pipelineStaleMissingProofRequirements: Array.isArray(sourceState.pipelineStaleMissingProofRequirements)
  1092. ? sourceState.pipelineStaleMissingProofRequirements
  1093. : []
  1094. };
  1095. }
  1096. function normalizeBusinessProofNextTopActions(actions, limit = 5) {
  1097. if (!Array.isArray(actions)) return [];
  1098. return actions
  1099. .slice()
  1100. .sort((left, right) => actionSortValue(left) - actionSortValue(right))
  1101. .slice(0, limit)
  1102. .map(item => ({
  1103. id: item.id || '',
  1104. source: item.source || '',
  1105. order: Number(item.order || 0),
  1106. priority: Number(item.priority || 0),
  1107. owner: item.owner || '',
  1108. title: item.title || '',
  1109. status: item.status || '',
  1110. evidence: item.evidence || '',
  1111. command: item.command || '',
  1112. expectedArtifact: item.expectedArtifact || '',
  1113. acceptance: item.acceptance || '',
  1114. missingProofRequirementCount: Array.isArray(item.missingProofRequirements) ? item.missingProofRequirements.length : 0,
  1115. missingProofRequirements: Array.isArray(item.missingProofRequirements) ? item.missingProofRequirements : []
  1116. }));
  1117. }
  1118. function actionSortValue(item) {
  1119. const priority = Number(item?.priority);
  1120. const order = Number(item?.order);
  1121. const primary = Number.isFinite(priority) && priority > 0 ? priority : 999999;
  1122. const secondary = Number.isFinite(order) && order > 0 ? order / 100000 : 0;
  1123. return primary + secondary;
  1124. }
  1125. function normalizeOwnerArtifacts(root, dir, summary) {
  1126. const artifacts = Array.isArray(summary?.ownerArtifacts) && summary.ownerArtifacts.length
  1127. ? summary.ownerArtifacts
  1128. : (summary?.ownerGroups || []).map(group => {
  1129. const slug = ownerSlug(group.owner);
  1130. return {
  1131. owner: group.owner,
  1132. actionCount: group.actionCount,
  1133. topPriority: group.topPriority,
  1134. markdown: path.join('by-owner', `${slug}.md`).replace(/\\/g, '/'),
  1135. csv: path.join('by-owner', `${slug}.csv`).replace(/\\/g, '/'),
  1136. gapIds: group.gapIds || [],
  1137. evidenceRowCount: 0,
  1138. nextActionCount: 0,
  1139. repairArtifacts: []
  1140. };
  1141. });
  1142. return artifacts.map(item => {
  1143. const markdown = path.join(dir, item.markdown || '');
  1144. const csv = path.join(dir, item.csv || '');
  1145. return {
  1146. owner: item.owner,
  1147. actionCount: Number(item.actionCount || 0),
  1148. topPriority: Number(item.topPriority || 0),
  1149. markdown: relIfExists(root, markdown),
  1150. csv: relIfExists(root, csv),
  1151. gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
  1152. evidenceRowCount: Number(item.evidenceRowCount || 0),
  1153. nextActionCount: Number(item.nextActionCount || 0),
  1154. repairArtifacts: normalizeRepairArtifacts(item.repairArtifacts || [])
  1155. };
  1156. });
  1157. }
  1158. function normalizePassThroughOwnerArtifacts(root, ownerArtifacts) {
  1159. if (!Array.isArray(ownerArtifacts)) return [];
  1160. return ownerArtifacts.map(item => ({
  1161. owner: item.owner || '',
  1162. actionCount: Number(item.actionCount || 0),
  1163. topPriority: Number(item.topPriority || 0),
  1164. markdown: relIfPossible(root, item.markdown || ''),
  1165. csv: relIfPossible(root, item.csv || ''),
  1166. gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
  1167. evidenceRowCount: Number(item.evidenceRowCount || 0),
  1168. nextActionCount: Number(item.nextActionCount || 0),
  1169. repairArtifacts: normalizeRepairArtifacts(item.repairArtifacts || [])
  1170. }));
  1171. }
  1172. function normalizeWorkOrderOwnerArtifacts(root, ownerArtifacts) {
  1173. if (!Array.isArray(ownerArtifacts)) return [];
  1174. return ownerArtifacts.map(item => ({
  1175. owner: item.owner || '',
  1176. workOrderCount: Number(item.workOrderCount || item.actionCount || 0),
  1177. topPriority: Number(item.topPriority || 0),
  1178. markdown: relIfPossible(root, item.markdown || ''),
  1179. csv: relIfPossible(root, item.csv || ''),
  1180. targetFiles: Array.isArray(item.targetFiles) ? item.targetFiles : [],
  1181. gapIds: Array.isArray(item.gapIds) ? item.gapIds : []
  1182. }));
  1183. }
  1184. function normalizeRepairArtifacts(repairArtifacts) {
  1185. return repairArtifacts.map(item => ({
  1186. title: item.title || '',
  1187. csv: item.csv || '',
  1188. actionCount: Number(item.actionCount || 0),
  1189. topPriority: Number(item.topPriority || 0),
  1190. fields: Array.isArray(item.fields) ? item.fields : []
  1191. }));
  1192. }
  1193. function renderRepairArtifactSummary(repairArtifacts) {
  1194. if (!repairArtifacts.length) return '无';
  1195. return repairArtifacts
  1196. .map(item => `${item.title || '修复清单'}:${item.csv || '缺失'}(${item.actionCount || 0})`)
  1197. .join(';');
  1198. }
  1199. function ownerSlug(owner) {
  1200. if (owner === '商务') return 'business';
  1201. if (owner === '商务/投放') return 'business-media';
  1202. if (owner === '技术/AI') return 'tech-ai';
  1203. return String(owner || 'owner').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'owner';
  1204. }
  1205. function fileGroup(root, dir, files) {
  1206. return Object.fromEntries(Object.entries(files).map(([key, file]) => [
  1207. key,
  1208. relIfExists(root, path.join(dir, file))
  1209. ]));
  1210. }
  1211. function latestOutputDir(outputsDir, pattern) {
  1212. if (!fs.existsSync(outputsDir)) return null;
  1213. const dirs = fs.readdirSync(outputsDir, { withFileTypes: true })
  1214. .filter(entry => entry.isDirectory() && pattern.test(entry.name))
  1215. .map(entry => path.join(outputsDir, entry.name))
  1216. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
  1217. return dirs[0] || null;
  1218. }
  1219. function readJsonIfExists(file) {
  1220. if (!fs.existsSync(file)) return null;
  1221. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  1222. }
  1223. function relIfExists(root, file) {
  1224. return fs.existsSync(file) ? path.relative(root, file).replace(/\\/g, '/') : null;
  1225. }
  1226. function relIfPossible(root, file) {
  1227. if (!file) return '';
  1228. const resolvedRoot = path.resolve(root);
  1229. const resolvedFile = path.resolve(file);
  1230. const relative = path.relative(resolvedRoot, resolvedFile);
  1231. if (relative && (relative.startsWith('..') || path.isAbsolute(relative))) return String(file).replace(/\\/g, '/');
  1232. return relative.replace(/\\/g, '/');
  1233. }
  1234. function finalDuplicateGroupCount(duplicateAudit) {
  1235. if (!duplicateAudit) return null;
  1236. return Object.values(duplicateAudit)
  1237. .reduce((sum, groups) => sum + (Array.isArray(groups) ? groups.length : 0), 0);
  1238. }
  1239. function countRowsWithMissingProofRequirements(rows) {
  1240. if (!Array.isArray(rows)) return 0;
  1241. return rows.filter(row =>
  1242. Array.isArray(row.missingProofRequirements) &&
  1243. row.missingProofRequirements.length >= 1
  1244. ).length;
  1245. }
  1246. function uniqueMissingProofRequirementCount(rows) {
  1247. if (!Array.isArray(rows)) return 0;
  1248. const values = new Set();
  1249. for (const row of rows) {
  1250. const requirements = Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements : [];
  1251. for (const requirement of requirements) {
  1252. if (requirement) values.add(String(requirement));
  1253. }
  1254. }
  1255. return values.size;
  1256. }
  1257. function tableRow(cells) {
  1258. return `| ${cells.map(cell => String(cell ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ')).join(' | ')} |`;
  1259. }
  1260. function parseArgs(argv) {
  1261. const result = {};
  1262. for (let index = 0; index < argv.length; index += 1) {
  1263. const arg = argv[index];
  1264. if (!arg.startsWith('--')) continue;
  1265. const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  1266. const next = argv[index + 1];
  1267. if (!next || next.startsWith('--')) result[key] = true;
  1268. else {
  1269. result[key] = next;
  1270. index += 1;
  1271. }
  1272. }
  1273. return result;
  1274. }
  1275. function withBom(text) {
  1276. return `\uFEFF${text}`;
  1277. }
  1278. if (require.main === module) main();
  1279. module.exports = {
  1280. buildLatestFormIndex
  1281. };