optimization-completion-audit.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const OUTPUTS = path.join(ROOT, 'outputs');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputsDir = path.resolve(args.outputs || OUTPUTS);
  9. const outputDir = path.resolve(args.output || process.env.TIHAO_OPTIMIZATION_COMPLETION_OUTPUT || path.join(outputsDir, `optimization-completion-${Date.now()}`));
  10. const overnightFile = args.overnight || bestOvernightAggregate(outputsDir) || latestFile(outputsDir, 'aggregate-summary.json');
  11. const historyFile = args.historyAudit || bestHistoryAudit(outputsDir) || latestFile(outputsDir, 'historical-dataset-audit.json');
  12. const customerEffectFile = args.customerEffect || bestCustomerEffectSummary(outputsDir, historyFile, ROOT) || latestFile(outputsDir, 'customer-effect-summary.json');
  13. const videoAbFile = args.videoAb || process.env.TIHAO_VIDEO_AB_SUMMARY || latestFile(outputsDir, 'video-hit-rate-summary.json');
  14. const videoAbPreflightFile = args.videoAbPreflight || process.env.TIHAO_VIDEO_AB_PREFLIGHT_SUMMARY || latestFile(outputsDir, 'video-hit-rate-preflight-summary.json');
  15. const summary = buildCompletionSummary({
  16. root: ROOT,
  17. outputsDir,
  18. sourceFiles: {
  19. overnight: relIfInside(ROOT, overnightFile),
  20. history: relIfInside(ROOT, historyFile),
  21. customerEffect: relIfInside(ROOT, customerEffectFile),
  22. videoAb: relIfInside(ROOT, videoAbFile),
  23. videoAbPreflight: relIfInside(ROOT, videoAbPreflightFile)
  24. },
  25. history: readJsonArg(historyFile),
  26. intakeReadiness: readJsonArg(args.intakeReadiness || latestFile(outputsDir, 'intake-readiness-summary.json')),
  27. videoReadiness: readJsonArg(args.videoReadiness || latestFile(outputsDir, 'video-resource-readiness-summary.json')),
  28. proofGapClosure: readJsonArg(args.proofGapClosure || latestFile(outputsDir, 'proof-gap-closure-summary.json')),
  29. handoff: readJsonArg(args.handoff || latestFile(outputsDir, 'optimization-handoff-summary.json')),
  30. businessProgress: readJsonArg(args.businessProgress || latestFile(outputsDir, 'business-proof-progress-summary.json')),
  31. longRunReadiness: readJsonArg(args.longRunReadiness || latestFile(outputsDir, 'long-run-readiness-summary.json')),
  32. videoAb: readJsonArg(videoAbFile),
  33. videoAbPreflight: readJsonArg(videoAbPreflightFile),
  34. overnight: readJsonArg(overnightFile),
  35. customerEffect: readJsonArg(customerEffectFile),
  36. optimizationPipeline: readJsonArg(args.optimizationPipeline || latestFile(outputsDir, 'optimization-pipeline-summary.json'))
  37. });
  38. fs.mkdirSync(outputDir, { recursive: true });
  39. const jsonPath = path.join(outputDir, 'optimization-completion-summary.json');
  40. const reportPath = path.join(outputDir, 'optimization-completion-report.md');
  41. fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
  42. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  43. console.log(JSON.stringify({
  44. outputDir,
  45. json: jsonPath,
  46. report: reportPath,
  47. complete: summary.complete,
  48. readyForClaim: summary.readyForClaim,
  49. proofGapOpenCount: summary.proofGapOpenCount,
  50. blockingReasonCount: summary.blockingReasonCount
  51. }, null, 2));
  52. if ((args.strict || process.env.TIHAO_OPTIMIZATION_COMPLETION_STRICT === 'true') && !summary.complete) {
  53. process.exitCode = 2;
  54. }
  55. }
  56. function buildCompletionSummary({
  57. root,
  58. outputsDir,
  59. history,
  60. intakeReadiness,
  61. videoReadiness,
  62. proofGapClosure,
  63. handoff,
  64. businessProgress,
  65. longRunReadiness,
  66. videoAb,
  67. videoAbPreflight,
  68. overnight,
  69. customerEffect,
  70. optimizationPipeline,
  71. sourceFiles = {}
  72. }) {
  73. const optimizationPipelineFreshness = getOptimizationPipelineFreshness(optimizationPipeline, {
  74. history,
  75. intakeReadiness,
  76. videoReadiness,
  77. proofGapClosure,
  78. customerEffect
  79. });
  80. const intakeReadinessMissingRequirements = getIntakeReadinessMissingRequirements(intakeReadiness);
  81. const videoResourceMissingRequirements = getVideoResourceMissingRequirements(videoReadiness);
  82. const proofGapClosureMissingRequirements = getProofGapClosureMissingRequirements(proofGapClosure);
  83. const videoAbMissingRequirements = getVideoAbMissingRequirements(videoAb, videoAbPreflight);
  84. const overnightMissingRequirements = getOvernightMissingRequirements(overnight);
  85. const customerEffectMissingRequirements = getCustomerEffectMissingRequirements(customerEffect, history, sourceFiles, root);
  86. const optimizationPipelineMissingRequirements = getOptimizationPipelineMissingRequirements(optimizationPipeline, optimizationPipelineFreshness);
  87. const handoffMissingRequirements = getHandoffMissingRequirements(handoff);
  88. const businessProgressMissingRequirements = getBusinessProofProgressMissingRequirements(businessProgress);
  89. const longRunReadinessMissingRequirements = getLongRunReadinessMissingRequirements(longRunReadiness);
  90. const gates = [
  91. gate({
  92. id: 'intake-readiness',
  93. title: '真实材料 intake readiness',
  94. passed: intakeReadinessMissingRequirements.length === 0,
  95. evidence: intakeReadiness
  96. ? `overallReady=${Boolean(intakeReadiness.acceptance?.overallReady)}, failureCount=${intakeReadiness.failureCount ?? 'unknown'}`
  97. : 'missing intake-readiness-summary.json',
  98. required: 'intake:readiness 必须 overallReady=true 且 failureCount=0。',
  99. command: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest',
  100. missingProofRequirements: intakeReadinessMissingRequirements
  101. }),
  102. gate({
  103. id: 'video-resource-readiness',
  104. title: '真实候选视频资源 readiness',
  105. passed: videoResourceMissingRequirements.length === 0,
  106. evidence: videoReadiness
  107. ? `readyForVideoAbPreflight=${Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight)}, failureCount=${videoReadiness.failureCount ?? 'unknown'}, realCandidateRows=${videoReadiness.counts?.realCandidateRows ?? 'unknown'}`
  108. : 'missing video-resource-readiness-summary.json',
  109. required: 'video:resource-readiness 必须 readyForVideoAbPreflight=true、failureCount=0,且包含真实候选视频。',
  110. command: 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict',
  111. missingProofRequirements: videoResourceMissingRequirements
  112. }),
  113. gate({
  114. id: 'proof-gap-closure',
  115. title: '真实证明缺口全部关闭',
  116. passed: proofGapClosureMissingRequirements.length === 0,
  117. evidence: proofGapClosure
  118. ? `complete=${Boolean(proofGapClosure.complete)}, openCount=${proofGapClosure.openCount ?? 'unknown'}, closedCount=${proofGapClosure.closedCount ?? 'unknown'}`
  119. : 'missing proof-gap-closure-summary.json',
  120. required: 'proof-gap:closure 必须 complete=true、openCount=0。',
  121. command: 'npm run proof-gap:closure -- --output outputs\\proof-gap-closure-latest',
  122. missingProofRequirements: proofGapClosureMissingRequirements
  123. }),
  124. gate({
  125. id: 'video-ab-live-proof',
  126. title: '真实视频 A/B live proof',
  127. passed: videoAbMissingRequirements.length === 0,
  128. evidence: formatVideoAbEvidence(videoAb, videoAbPreflight),
  129. required: 'acceptance:video-ab 必须在 live 模式、真实 provider、真实视频资源下通过。',
  130. command: 'npm run acceptance:video-ab',
  131. missingProofRequirements: videoAbMissingRequirements
  132. }),
  133. gate({
  134. id: 'live-provider-overnight-proof',
  135. title: '真实 live/provider 长跑证明',
  136. passed: overnightMissingRequirements.length === 0,
  137. evidence: overnight
  138. ? `liveEnabled=${Boolean(overnight.manifest?.liveEnabled)}, overallPass=${Boolean(overnight.acceptance?.overallPass)}, failureCount=${overnight.failureCount ?? 'unknown'}, failedGateCount=${overnight.acceptance?.failedGateCount ?? overnight.failedGateCount ?? 'unknown'}, source=${sourceFiles.overnight || 'unknown'}`
  139. : 'missing aggregate-summary.json',
  140. required: 'overnight aggregate 必须 liveEnabled=true、overallPass=true、failureCount=0、failedGateCount=0。',
  141. command: 'npm run longrun:readiness;真实 provider 就绪后运行 npm run overnight:quality',
  142. missingProofRequirements: overnightMissingRequirements
  143. }),
  144. gate({
  145. id: 'customer-effect-proof',
  146. title: '商务复核和客户效果证明',
  147. passed: customerEffectMissingRequirements.length === 0,
  148. evidence: customerEffect
  149. ? `overallPass=${Boolean(customerEffect.acceptance?.overallPass)}, customerSelectedRate=${customerEffect.customerSelectedRate ?? 'unknown'}, referenceCustomerSelectedRate=${customerEffect.referenceCustomerSelectedRate ?? 'unknown'}, manualSupplementReductionRate=${customerEffect.manualSupplementReductionRate ?? 'unknown'}, historyAuditPath=${customerEffect.historyAuditPath || 'missing'}`
  150. : 'missing customer-effect-summary.json',
  151. required: 'customer-effect:audit 必须 overallPass=true,并带真实客户选择、拒绝归因和人工补号量。',
  152. command: 'npm run customer-effect:audit -- --review-csv <已标注CSV> --history-audit <historical-dataset-audit.json> --current-manual-supplement-count <本轮人工补号量> --output <客户效果输出目录> --strict',
  153. missingProofRequirements: customerEffectMissingRequirements
  154. }),
  155. gate({
  156. id: 'optimization-pipeline',
  157. title: '端到端优化 pipeline 可声明状态',
  158. passed: optimizationPipelineMissingRequirements.length === 0,
  159. evidence: formatOptimizationPipelineEvidence(optimizationPipeline, optimizationPipelineFreshness),
  160. required: 'optimization:pipeline 必须在真实材料下 readyForClaim=true,且 proof-gap closure 已关闭。',
  161. 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',
  162. missingProofRequirements: optimizationPipelineMissingRequirements
  163. }),
  164. gate({
  165. id: 'handoff-complete',
  166. title: '交接摘要完成状态',
  167. passed: handoffMissingRequirements.length === 0,
  168. evidence: handoff
  169. ? `complete=${Boolean(handoff.complete)}, ready=${handoff.readyCapabilities?.length ?? 0}, readyNotProven=${handoff.readyNotProven?.length ?? 0}, blockers=${handoff.externalBlockers?.length ?? 0}`
  170. : 'missing optimization-handoff-summary.json',
  171. required: 'handoff summary 必须 complete=true,且不得有未证明或外部阻塞项。',
  172. command: 'npm run handoff:summary -- --output outputs\\optimization-handoff-latest',
  173. missingProofRequirements: handoffMissingRequirements
  174. }),
  175. gate({
  176. id: 'business-proof-progress',
  177. title: '15 步补证进度全部通过',
  178. passed: businessProgressMissingRequirements.length === 0,
  179. evidence: businessProgress
  180. ? `complete=${Boolean(businessProgress.complete)}, pass=${businessProgress.counts?.pass ?? 0}, fail=${businessProgress.counts?.fail ?? 0}, pending=${businessProgress.counts?.pending ?? 0}, blocked=${businessProgress.counts?.blocked_by_external_data ?? 0}`
  181. : 'missing business-proof-progress-summary.json',
  182. required: 'business-proof:progress 必须 complete=true,15 步补证均 pass。',
  183. command: 'npm run business-proof:progress -- --outputs outputs --output outputs\\business-proof-progress-latest',
  184. missingProofRequirements: businessProgressMissingRequirements
  185. }),
  186. gate({
  187. id: 'longrun-readiness',
  188. title: '真实长跑启动前门禁',
  189. passed: longRunReadinessMissingRequirements.length === 0,
  190. evidence: longRunReadiness
  191. ? `ready=${Boolean(longRunReadiness.ready)}, fail=${longRunReadiness.counts?.fail ?? 0}, pass=${longRunReadiness.counts?.pass ?? 0}`
  192. : 'missing long-run-readiness-summary.json',
  193. required: 'longrun:readiness 必须 ready=true 且 fail=0。',
  194. command: 'npm run longrun:readiness',
  195. missingProofRequirements: longRunReadinessMissingRequirements
  196. })
  197. ];
  198. const blockingReasons = gates
  199. .filter(item => !item.passed)
  200. .map(item => ({
  201. id: item.id,
  202. title: item.title,
  203. evidence: item.evidence,
  204. required: item.required,
  205. missingProofRequirements: item.missingProofRequirements,
  206. command: item.command
  207. }));
  208. const proofGapOpenCount = Number(proofGapClosure?.openCount ?? 0);
  209. const complete = gates.every(item => item.passed);
  210. const readyForClaim = complete && proofGapOpenCount === 0;
  211. const blockingReasonCount = blockingReasons.length;
  212. const blockingReasonsWithMissingProofRequirements = countRowsWithMissingProofRequirements(blockingReasons);
  213. const missingProofRequirementCount = uniqueMissingProofRequirementCount(blockingReasons);
  214. return {
  215. generatedAt: new Date().toISOString(),
  216. root,
  217. outputsDir,
  218. sourceFiles,
  219. materialType: 'optimization_completion_audit',
  220. directCustomerProof: false,
  221. proofLevel: readyForClaim ? 'real_evidence' : 'not_business_proof',
  222. complete,
  223. readyForClaim,
  224. proofGapOpenCount,
  225. proofGapClosedCount: Number(proofGapClosure?.closedCount ?? 0),
  226. intakeFailureCount: Number(intakeReadiness?.failureCount ?? 0),
  227. videoResourceFailureCount: Number(videoReadiness?.failureCount ?? 0),
  228. realCandidateRows: Number(videoReadiness?.counts?.realCandidateRows ?? 0),
  229. videoAbPreflightReadyForVideoAb: Boolean(videoAbPreflight?.readyForVideoAb),
  230. videoAbPreflightFailureCount: videoAbPreflight ? Number(videoAbPreflight.failureCount ?? 0) : null,
  231. blockingReasonCount,
  232. blockingReasonsWithMissingProofRequirements,
  233. missingProofRequirementCount,
  234. blockingReasons,
  235. gates,
  236. canClaim: {
  237. tihaoRateImproved: readyForClaim,
  238. customerEffectAchieved: readyForClaim,
  239. manualSupplementReduced: readyForClaim
  240. },
  241. forbiddenClaimsWhenIncomplete: [
  242. '提号率已经提升',
  243. '客户效果已经达标',
  244. '人工补号量已经下降'
  245. ],
  246. nextRequiredCommands: [
  247. 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest',
  248. 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict',
  249. 'npm run proof-gap:closure -- --output outputs\\proof-gap-closure-latest',
  250. 'npm run optimization:pipeline -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict',
  251. 'npm run round:refresh -- --output-root outputs --strict'
  252. ],
  253. guardrails: [
  254. '本审计是完成度判定入口;审计产物本身不是业务证明。',
  255. 'real-proof-intake-bundle、proof-gap-request、proof-gap-operator-pack、索引和执行表都是协调/收集材料入口,不是业务证明。',
  256. '缺真实材料、真实候选视频、live/provider 长跑或客户效果证明时,不得宣称提号率提升、客户效果达标或人工补号量下降。',
  257. '不得在输入、报告、模板或日志中写入 sessionToken、Authorization、模型 token 或 npm token。'
  258. ]
  259. };
  260. }
  261. function gate({ id, title, passed, evidence, required, command, missingProofRequirements = [] }) {
  262. return {
  263. id,
  264. title,
  265. status: passed ? 'pass' : 'blocked',
  266. passed: Boolean(passed),
  267. evidence,
  268. required,
  269. command,
  270. missingProofRequirements
  271. };
  272. }
  273. function getIntakeReadinessMissingRequirements(intakeReadiness) {
  274. if (!intakeReadiness) return ['missing intake-readiness-summary.json'];
  275. const missing = [];
  276. const acceptance = intakeReadiness.acceptance || {};
  277. if (acceptance.overallReady !== true) missing.push('acceptance.overallReady=true');
  278. if (Number(intakeReadiness.failureCount || 0) !== 0) missing.push('failureCount=0');
  279. if (acceptance.historyReady !== true) missing.push('acceptance.historyReady=true');
  280. if (acceptance.videoReady !== true) missing.push('acceptance.videoReady=true');
  281. if (acceptance.reviewMetricsReady !== true) missing.push('acceptance.reviewMetricsReady=true');
  282. if (acceptance.customerEffectReady !== true) missing.push('acceptance.customerEffectReady=true');
  283. return missing;
  284. }
  285. function getVideoResourceMissingRequirements(videoReadiness) {
  286. if (!videoReadiness) return ['missing video-resource-readiness-summary.json'];
  287. const missing = [];
  288. if (videoReadiness.acceptance?.readyForVideoAbPreflight !== true) missing.push('acceptance.readyForVideoAbPreflight=true');
  289. if (Number(videoReadiness.failureCount || 0) !== 0) missing.push('failureCount=0');
  290. if (Number(videoReadiness.counts?.realCandidateRows || 0) < 1) missing.push('counts.realCandidateRows>=1');
  291. if (Number(videoReadiness.counts?.realReferenceRows || 0) < 1) missing.push('counts.realReferenceRows>=1');
  292. if (Number(videoReadiness.counts?.videoUrlRows || 0) < 1) missing.push('counts.videoUrlRows>=1');
  293. return missing;
  294. }
  295. function getProofGapClosureMissingRequirements(proofGapClosure) {
  296. if (!proofGapClosure) return ['missing proof-gap-closure-summary.json'];
  297. const missing = [];
  298. if (proofGapClosure.complete !== true) missing.push('proofGapClosure.complete=true');
  299. if (Number(proofGapClosure.openCount || 0) !== 0) missing.push('proofGapClosure.openCount=0');
  300. if (Number(proofGapClosure.closedCount || 0) < 5) missing.push('proofGapClosure.closedCount>=5');
  301. if (Number(proofGapClosure.businessGapCount || 0) < 5) missing.push('proofGapClosure.businessGapCount>=5');
  302. const rows = Array.isArray(proofGapClosure.rows) ? proofGapClosure.rows : [];
  303. const businessRows = rows.filter(row => row.id !== 'intake-readiness');
  304. if (businessRows.length < 5) missing.push('proofGapClosure.businessRows.length>=5');
  305. for (const row of rows) {
  306. if (row.status === 'closed' || row.id === 'intake-readiness') continue;
  307. for (const requirement of row.missingProofRequirements || []) {
  308. missing.push(`${row.id}: ${requirement}`);
  309. }
  310. }
  311. return uniqueStrings(missing);
  312. }
  313. function isRealVideoAbProof(videoAb) {
  314. return getVideoAbMissingRequirements(videoAb).length === 0;
  315. }
  316. function getVideoAbMissingRequirements(videoAb, videoAbPreflight = null) {
  317. const missing = [];
  318. if (!videoAb) {
  319. missing.push('missing video-hit-rate-summary.json');
  320. } else {
  321. if (!videoAb.acceptance?.passed) missing.push('acceptance.passed=true');
  322. const context = videoAb.proofContext || {};
  323. if (context.mode !== 'live') missing.push('proofContext.mode=live');
  324. if (context.collectionMode !== 'live') missing.push('proofContext.collectionMode=live');
  325. if (context.generatedBy !== 'acceptance:video-ab') missing.push('proofContext.generatedBy=acceptance:video-ab');
  326. if (context.requiresRuntimeCredential !== true) missing.push('proofContext.requiresRuntimeCredential=true');
  327. if (context.requiresVocSocialProvider !== true) missing.push('proofContext.requiresVocSocialProvider=true');
  328. if (context.requiresVideoAnalysisProvider !== true) missing.push('proofContext.requiresVideoAnalysisProvider=true');
  329. if (videoAb.enhanced?.provider?.evidence?.providerStatus !== 'ok') missing.push('enhanced.provider.evidence.providerStatus=ok');
  330. }
  331. if (missing.length) {
  332. missing.push(...getVideoAbPreflightMissingRequirements(videoAbPreflight));
  333. }
  334. return uniqueStrings(missing);
  335. }
  336. function getVideoAbPreflightMissingRequirements(videoAbPreflight) {
  337. if (!videoAbPreflight) return [];
  338. const missing = [];
  339. if (videoAbPreflight.proofLevel !== 'not_business_proof') missing.push('videoAbPreflight.proofLevel=not_business_proof');
  340. if (videoAbPreflight.canCloseProofGap !== false) missing.push('videoAbPreflight.canCloseProofGap=false');
  341. if (videoAbPreflight.proofContext?.generatedBy !== 'acceptance:video-ab-preflight') missing.push('videoAbPreflight.proofContext.generatedBy=acceptance:video-ab-preflight');
  342. if (videoAbPreflight.readyForVideoAb !== true) {
  343. missing.push('videoAbPreflight.readyForVideoAb=true');
  344. for (const requirement of videoAbPreflight.missingProofRequirements || []) {
  345. missing.push(`videoAbPreflight.${requirement}`);
  346. }
  347. }
  348. return missing;
  349. }
  350. function formatVideoAbEvidence(videoAb, videoAbPreflight) {
  351. const liveEvidence = videoAb
  352. ? `passed=${Boolean(videoAb.acceptance?.passed)}, mode=${videoAb.proofContext?.mode || 'unknown'}, collectionMode=${videoAb.proofContext?.collectionMode || 'unknown'}, generatedBy=${videoAb.proofContext?.generatedBy || 'unknown'}, provider=${videoAb.enhanced?.provider?.evidence?.providerStatus || 'unknown'}`
  353. : 'missing video-hit-rate-summary.json';
  354. if (!videoAbPreflight || getVideoAbMissingRequirements(videoAb).length === 0) return liveEvidence;
  355. const runtime = videoAbPreflight.runtime || {};
  356. const preflightEvidence = [
  357. `preflight.readyForVideoAb=${Boolean(videoAbPreflight.readyForVideoAb)}`,
  358. `preflight.failureCount=${videoAbPreflight.failureCount ?? 'unknown'}`,
  359. `preflight.generatedBy=${videoAbPreflight.proofContext?.generatedBy || 'unknown'}`,
  360. `preflight.proofLevel=${videoAbPreflight.proofLevel || 'unknown'}`,
  361. `preflight.canCloseProofGap=${Boolean(videoAbPreflight.canCloseProofGap)}`,
  362. `runtimeCredentialPresent=${Boolean(runtime.runtimeCredentialPresent)}`,
  363. `companyResolved=${Boolean(runtime.companyResolved)}`,
  364. `vocSocialProviderTokenPresent=${Boolean(runtime.vocSocialProviderTokenPresent)}`,
  365. `videoAnalysisTokenPresent=${Boolean(runtime.videoAnalysisTokenPresent)}`
  366. ].join(', ');
  367. return `${liveEvidence}; ${preflightEvidence}`;
  368. }
  369. function getOvernightMissingRequirements(overnight) {
  370. if (!overnight) return ['missing aggregate-summary.json'];
  371. if (overnight.manifest?.liveEnabled === true &&
  372. overnight.acceptance?.overallPass === true &&
  373. Number(overnight.failureCount || 0) === 0 &&
  374. Number(overnight.acceptance?.failedGateCount ?? overnight.failedGateCount ?? 0) === 0 &&
  375. overnight.acceptance?.tokenLeak !== true) {
  376. return [];
  377. }
  378. const missing = [];
  379. if (overnight.manifest?.liveEnabled !== true) missing.push('manifest.liveEnabled=true');
  380. if (overnight.acceptance?.overallPass !== true) missing.push('acceptance.overallPass=true');
  381. if (Number(overnight.failureCount || 0) !== 0) missing.push('failureCount=0');
  382. const failedGateCount = Number(overnight.acceptance?.failedGateCount ?? overnight.failedGateCount ?? 0);
  383. if (failedGateCount !== 0) missing.push('failedGateCount=0');
  384. if (overnight.acceptance?.tokenLeak === true) missing.push('acceptance.tokenLeak=false');
  385. return missing;
  386. }
  387. function bestOvernightAggregate(outputsDir) {
  388. const files = findFiles(outputsDir, 'aggregate-summary.json');
  389. const annotated = files
  390. .map(file => ({ file, json: readJsonArg(file), mtimeMs: fs.statSync(file).mtimeMs }))
  391. .filter(item => item.json);
  392. const closable = annotated
  393. .filter(item => isClosableOvernightAggregate(item.json))
  394. .sort((a, b) => b.mtimeMs - a.mtimeMs);
  395. if (closable.length) return closable[0].file;
  396. return annotated.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.file || '';
  397. }
  398. function findFiles(dir, fileName) {
  399. if (!fs.existsSync(dir)) return [];
  400. const result = [];
  401. const stack = [dir];
  402. while (stack.length) {
  403. const current = stack.pop();
  404. for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
  405. const full = path.join(current, entry.name);
  406. if (entry.isDirectory()) stack.push(full);
  407. else if (entry.isFile() && entry.name === fileName) result.push(full);
  408. }
  409. }
  410. return result;
  411. }
  412. function isClosableOvernightAggregate(aggregate) {
  413. return Boolean(
  414. aggregate?.manifest?.liveEnabled === true &&
  415. aggregate?.acceptance?.overallPass === true &&
  416. Number(aggregate?.failureCount || 0) === 0 &&
  417. Number(aggregate?.acceptance?.failedGateCount ?? aggregate?.failedGateCount ?? 0) === 0 &&
  418. aggregate?.acceptance?.tokenLeak !== true
  419. );
  420. }
  421. function getCustomerEffectMissingRequirements(customerEffect, history, sourceFiles = {}, root = process.cwd()) {
  422. if (!customerEffect) return ['missing customer-effect-summary.json'];
  423. const missing = [];
  424. const acceptance = customerEffect.acceptance || {};
  425. if (acceptance.overallPass !== true) missing.push('acceptance.overallPass=true');
  426. if (acceptance.reviewMetricsOverallUsable !== true) missing.push('acceptance.reviewMetricsOverallUsable=true');
  427. if (acceptance.customerSelectedRateMeasured !== true) missing.push('acceptance.customerSelectedRateMeasured=true');
  428. if (acceptance.customerSelectedRate30Pass !== true) missing.push('acceptance.customerSelectedRate30Pass=true');
  429. if (acceptance.referenceCustomerSelectedRateMeasured !== true) missing.push('acceptance.referenceCustomerSelectedRateMeasured=true');
  430. if (acceptance.referenceCustomerSelectedRate40Pass !== true) missing.push('acceptance.referenceCustomerSelectedRate40Pass=true');
  431. if (acceptance.historyReadyForCustomerEffectProof !== true) missing.push('acceptance.historyReadyForCustomerEffectProof=true');
  432. if (acceptance.manualSupplementBaselineAvailable !== true) missing.push('acceptance.manualSupplementBaselineAvailable=true');
  433. if (acceptance.currentManualSupplementAvailable !== true) missing.push('acceptance.currentManualSupplementAvailable=true');
  434. if (acceptance.manualSupplementReduction50Pass !== true) missing.push('acceptance.manualSupplementReduction50Pass=true');
  435. if (!customerEffect.reviewPath) missing.push('reviewPath=provided');
  436. if (!customerEffect.historyAuditPath) missing.push('historyAuditPath=provided');
  437. if (sourceFiles.history && customerEffect.historyAuditPath && !samePath(root, customerEffect.historyAuditPath, sourceFiles.history)) {
  438. missing.push('historyAuditPath=current-history-audit');
  439. }
  440. if (!history) {
  441. missing.push('currentHistoryAudit=present');
  442. } else {
  443. for (const requirement of getHistoryMissingRequirements(history)) {
  444. missing.push(`currentHistoryAudit.${requirement}`);
  445. }
  446. }
  447. if (!Number.isFinite(Number(customerEffect.customerSelectedRate))) missing.push('customerSelectedRate=number');
  448. if (!Number.isFinite(Number(customerEffect.referenceCustomerSelectedRate))) missing.push('referenceCustomerSelectedRate=number');
  449. if (!Number.isFinite(Number(customerEffect.manualSupplementReductionRate))) missing.push('manualSupplementReductionRate=number');
  450. if (customerEffect.manualSupplementBaseline?.available !== true) missing.push('manualSupplementBaseline.available=true');
  451. if (customerEffect.currentManualSupplement?.available !== true) missing.push('currentManualSupplement.available=true');
  452. return uniqueStrings([...(customerEffect.missingEvidence || []), ...missing]);
  453. }
  454. function getOptimizationPipelineFreshness(optimizationPipeline, sources = {}) {
  455. if (!optimizationPipeline) {
  456. return {
  457. current: false,
  458. reason: 'missing optimization-pipeline-summary.json'
  459. };
  460. }
  461. const pipelineTime = parseGeneratedAt(optimizationPipeline.generatedAt);
  462. const sourceTimes = [
  463. sources.history,
  464. sources.intakeReadiness,
  465. sources.videoReadiness,
  466. sources.proofGapClosure,
  467. sources.customerEffect
  468. ]
  469. .map(source => parseGeneratedAt(source?.generatedAt))
  470. .filter(value => Number.isFinite(value));
  471. if (!Number.isFinite(pipelineTime) || sourceTimes.length === 0) {
  472. return {
  473. current: true,
  474. reason: 'freshness not comparable',
  475. pipelineGeneratedAt: optimizationPipeline.generatedAt || ''
  476. };
  477. }
  478. const latestSourceTime = Math.max(...sourceTimes);
  479. const current = pipelineTime >= latestSourceTime;
  480. return {
  481. current,
  482. reason: current
  483. ? 'pipeline generated after current proof sources'
  484. : `stale optimization-pipeline-summary.json: generatedAt=${optimizationPipeline.generatedAt || 'unknown'} older than latest proof source=${new Date(latestSourceTime).toISOString()}`,
  485. pipelineGeneratedAt: optimizationPipeline.generatedAt || '',
  486. latestProofSourceGeneratedAt: new Date(latestSourceTime).toISOString()
  487. };
  488. }
  489. function parseGeneratedAt(value) {
  490. if (!value) return NaN;
  491. const time = Date.parse(value);
  492. return Number.isFinite(time) ? time : NaN;
  493. }
  494. function getHistoryMissingRequirements(history) {
  495. if (!history) return ['missing historical-dataset-audit.json'];
  496. const missing = [];
  497. const acceptance = history.acceptance || {};
  498. if (acceptance.readyForCustomerEffectProof !== true) missing.push('acceptance.readyForCustomerEffectProof=true');
  499. if (Number(history.briefCount || 0) < 5) missing.push('briefCount>=5');
  500. if (acceptance.parseOk !== true) missing.push('acceptance.parseOk=true');
  501. if (acceptance.minBriefsMet !== true) missing.push('acceptance.minBriefsMet=true');
  502. if (acceptance.allHaveBriefText !== true) missing.push('acceptance.allHaveBriefText=true');
  503. if (acceptance.allHaveManualFinalList !== true) missing.push('acceptance.allHaveManualFinalList=true');
  504. if (acceptance.allHaveCustomerDecision !== true) missing.push('acceptance.allHaveCustomerDecision=true');
  505. if (acceptance.allHaveFeedbackReason !== true) missing.push('acceptance.allHaveFeedbackReason=true');
  506. if (acceptance.allHaveManualSupplementBaseline !== true) missing.push('acceptance.allHaveManualSupplementBaseline=true');
  507. if (acceptance.categoryCoverageMet !== true) missing.push('acceptance.categoryCoverageMet=true');
  508. if (Number(history.missingCriticalCount || 0) !== 0) missing.push('missingCriticalCount=0');
  509. if (!Array.isArray(history.items) || history.items.length < 5) missing.push('items.length>=5');
  510. if (Number(history.withManualFinalList || 0) < 5) missing.push('withManualFinalList>=5');
  511. if (Number(history.withCustomerDecision || 0) < 5) missing.push('withCustomerDecision>=5');
  512. if (Number(history.withRejectionReason || 0) < 1) missing.push('withRejectionReason>=1');
  513. if (Number(history.withManualSupplementBaseline || 0) < 5) missing.push('withManualSupplementBaseline>=5');
  514. return missing;
  515. }
  516. function samePath(root, left, right) {
  517. const normalize = value => {
  518. const resolved = path.resolve(root || process.cwd(), value || '');
  519. return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
  520. };
  521. return normalize(left) === normalize(right);
  522. }
  523. function formatOptimizationPipelineEvidence(optimizationPipeline, freshness) {
  524. if (!optimizationPipeline) return 'missing optimization-pipeline-summary.json';
  525. const base = `readyForClaim=${Boolean(optimizationPipeline.readyForClaim)}, proofGapClosure.complete=${Boolean(optimizationPipeline.proofGapClosure?.complete)}, proofGapClosure.openCount=${optimizationPipeline.proofGapClosure?.openCount ?? 'unknown'}, proofGapClosure.closedCount=${optimizationPipeline.proofGapClosure?.closedCount ?? 'unknown'}`;
  526. if (freshness?.current === false) return `${base}, stale=true, ${freshness.reason}`;
  527. return `${base}, stale=false`;
  528. }
  529. function getOptimizationPipelineMissingRequirements(optimizationPipeline, freshness = { current: true }) {
  530. if (!optimizationPipeline) return ['missing optimization-pipeline-summary.json'];
  531. if (optimizationPipeline.readyForClaim === true &&
  532. freshness.current !== false &&
  533. optimizationPipeline.proofGapClosure?.complete === true &&
  534. Number(optimizationPipeline.proofGapClosure?.openCount || 0) === 0 &&
  535. Number(optimizationPipeline.proofGapClosure?.closedCount || 0) >= 5 &&
  536. Number(optimizationPipeline.proofGapClosure?.businessGapCount || 0) >= 5) {
  537. return [];
  538. }
  539. const missing = [];
  540. if (freshness.current === false) missing.push('optimizationPipeline.generatedAt>=currentProofSources');
  541. if (optimizationPipeline.readyForClaim !== true) missing.push('optimizationPipeline.readyForClaim=true');
  542. if (optimizationPipeline.proofGapClosure?.complete !== true) missing.push('optimizationPipeline.proofGapClosure.complete=true');
  543. if (Number(optimizationPipeline.proofGapClosure?.openCount || 0) !== 0) missing.push('optimizationPipeline.proofGapClosure.openCount=0');
  544. if (Number(optimizationPipeline.proofGapClosure?.closedCount || 0) < 5) missing.push('optimizationPipeline.proofGapClosure.closedCount>=5');
  545. if (Number(optimizationPipeline.proofGapClosure?.businessGapCount || 0) < 5) missing.push('optimizationPipeline.proofGapClosure.businessGapCount>=5');
  546. const failedSteps = Array.isArray(optimizationPipeline.results)
  547. ? optimizationPipeline.results.filter(item => item.status && item.status !== 'pass')
  548. : [];
  549. for (const step of failedSteps) missing.push(`pipeline step ${step.id}=pass`);
  550. return uniqueStrings(missing);
  551. }
  552. function getHandoffMissingRequirements(handoff) {
  553. if (!handoff) return ['missing optimization-handoff-summary.json'];
  554. const missing = [];
  555. if (handoff.complete !== true) missing.push('handoff.complete=true');
  556. if ((handoff.readyNotProven || []).length) missing.push('handoff.readyNotProven.length=0');
  557. if ((handoff.externalBlockers || []).length) missing.push('handoff.externalBlockers.length=0');
  558. return missing;
  559. }
  560. function getBusinessProofProgressMissingRequirements(businessProgress) {
  561. if (!businessProgress) return ['missing business-proof-progress-summary.json'];
  562. const missing = [];
  563. const counts = businessProgress.counts || {};
  564. if (businessProgress.complete !== true) missing.push('businessProofProgress.complete=true');
  565. if (Number(counts.fail || 0) !== 0) missing.push('businessProofProgress.counts.fail=0');
  566. if (Number(counts.pending || 0) !== 0) missing.push('businessProofProgress.counts.pending=0');
  567. if (Number(counts.blocked_by_external_data || 0) !== 0) missing.push('businessProofProgress.counts.blocked_by_external_data=0');
  568. if (Number(counts.pass || 0) < 15) missing.push('businessProofProgress.counts.pass>=15');
  569. if (!Array.isArray(businessProgress.rows) || businessProgress.rows.length < 15) missing.push('businessProofProgress.rows.length>=15');
  570. const openRows = Array.isArray(businessProgress.rows)
  571. ? businessProgress.rows.filter(row => row.status && row.status !== 'pass')
  572. : [];
  573. for (const row of openRows) missing.push(`step-${row.order || '?'} ${row.title || '补证步骤'}=pass`);
  574. return uniqueStrings(missing);
  575. }
  576. function getLongRunReadinessMissingRequirements(longRunReadiness) {
  577. if (!longRunReadiness) return ['missing long-run-readiness-summary.json'];
  578. const missing = [];
  579. if (longRunReadiness.ready !== true) missing.push('longRunReadiness.ready=true');
  580. if (Number(longRunReadiness.counts?.fail || 0) !== 0) missing.push('longRunReadiness.counts.fail=0');
  581. const failedChecks = Array.isArray(longRunReadiness.checks)
  582. ? longRunReadiness.checks.filter(item => item.status === 'fail')
  583. : [];
  584. for (const item of failedChecks) missing.push(`${item.name}=pass`);
  585. return uniqueStrings(missing);
  586. }
  587. function uniqueStrings(values) {
  588. return [...new Set((values || []).filter(Boolean).map(value => String(value)))];
  589. }
  590. function countRowsWithMissingProofRequirements(rows) {
  591. if (!Array.isArray(rows)) return 0;
  592. return rows.filter(row =>
  593. Array.isArray(row.missingProofRequirements) &&
  594. row.missingProofRequirements.length >= 1
  595. ).length;
  596. }
  597. function uniqueMissingProofRequirementCount(rows) {
  598. if (!Array.isArray(rows)) return 0;
  599. const values = new Set();
  600. for (const row of rows) {
  601. const requirements = Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements : [];
  602. for (const requirement of requirements) {
  603. if (requirement) values.add(String(requirement));
  604. }
  605. }
  606. return values.size;
  607. }
  608. function renderReport(summary) {
  609. return [
  610. '# 提号长期优化完成度审计',
  611. '',
  612. `- 生成时间:${summary.generatedAt}`,
  613. `- 是否完成:${summary.complete ? '是' : '否'}`,
  614. `- 是否可声明业务效果:${summary.readyForClaim ? '是' : '否'}`,
  615. `- 证明等级:${summary.proofLevel}`,
  616. `- 直接客户证明:${summary.directCustomerProof ? '是' : '否'}`,
  617. `- proof-gap openCount:${summary.proofGapOpenCount}`,
  618. `- 阻塞项数量:${summary.blockingReasonCount}`,
  619. `- 带缺失证明要求的阻塞项:${summary.blockingReasonsWithMissingProofRequirements}`,
  620. `- 缺失证明要求唯一计数:${summary.missingProofRequirementCount}`,
  621. '',
  622. '## 禁用宣称',
  623. '',
  624. `- 提号率提升:${summary.canClaim.tihaoRateImproved ? '可声明' : '不可声明'}`,
  625. `- 客户效果达标:${summary.canClaim.customerEffectAchieved ? '可声明' : '不可声明'}`,
  626. `- 人工补号量下降:${summary.canClaim.manualSupplementReduced ? '可声明' : '不可声明'}`,
  627. '',
  628. '## 完成门槛',
  629. '',
  630. '| 门槛 | 状态 | 当前证据 | 缺失证明 | 通过标准 | 复验命令 |',
  631. '| --- | --- | --- | --- | --- | --- |',
  632. ...summary.gates.map(item => `| ${item.title} | ${item.status} | ${escapeCell(item.evidence)} | ${escapeCell((item.missingProofRequirements || []).join(';') || '-')} | ${escapeCell(item.required)} | \`${escapeCell(item.command)}\` |`),
  633. '',
  634. '## 下一步',
  635. '',
  636. ...summary.nextRequiredCommands.map(command => `- \`${command}\``),
  637. '',
  638. '## 边界',
  639. '',
  640. ...summary.guardrails.map(item => `- ${item}`)
  641. ].join('\n');
  642. }
  643. function latestFile(outputsDir, fileName) {
  644. if (!fs.existsSync(outputsDir)) return '';
  645. return fs.readdirSync(outputsDir, { withFileTypes: true })
  646. .filter(entry => entry.isDirectory())
  647. .map(entry => path.join(outputsDir, entry.name, fileName))
  648. .filter(file => fs.existsSync(file))
  649. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || '';
  650. }
  651. function bestHistoryAudit(outputsDir) {
  652. return bestProofArtifact(outputsDir, 'historical-dataset-audit.json', {
  653. isClaimable: item => getHistoryMissingRequirements(item.json).length === 0
  654. });
  655. }
  656. function bestCustomerEffectSummary(outputsDir, historyFile = '', root = process.cwd()) {
  657. const history = readJsonArg(historyFile);
  658. const sourceFiles = { history: relIfInside(root, historyFile) };
  659. return bestProofArtifact(outputsDir, 'customer-effect-summary.json', {
  660. isClaimable: item => getCustomerEffectMissingRequirements(item.json, history, sourceFiles, root).length === 0,
  661. score: item => {
  662. if (!historyFile || !item.json?.historyAuditPath) return 0;
  663. return samePath(root, item.json.historyAuditPath, historyFile) ? 2 : 0;
  664. }
  665. });
  666. }
  667. function bestProofArtifact(outputsDir, fileName, { isClaimable = () => false, score = () => 0 } = {}) {
  668. const annotated = findFiles(outputsDir, fileName)
  669. .filter(file => isBusinessArtifactPath(outputsDir, file))
  670. .map(file => ({ file, json: readJsonArg(file), mtimeMs: fs.statSync(file).mtimeMs }))
  671. .filter(item => item.json);
  672. if (!annotated.length) return '';
  673. return annotated
  674. .sort((left, right) => {
  675. const rightClaimable = safePredicate(isClaimable, right) ? 1 : 0;
  676. const leftClaimable = safePredicate(isClaimable, left) ? 1 : 0;
  677. if (rightClaimable !== leftClaimable) return rightClaimable - leftClaimable;
  678. const rightScore = Number(score(right) || 0);
  679. const leftScore = Number(score(left) || 0);
  680. if (rightScore !== leftScore) return rightScore - leftScore;
  681. return right.mtimeMs - left.mtimeMs;
  682. })[0].file;
  683. }
  684. function safePredicate(predicate, item) {
  685. try {
  686. return Boolean(predicate(item));
  687. } catch (_error) {
  688. return false;
  689. }
  690. }
  691. function isBusinessArtifactPath(outputsDir, file) {
  692. const relative = path.relative(outputsDir, file).replace(/\\/g, '/').toLowerCase();
  693. const segments = relative.split('/').filter(Boolean);
  694. return !segments.some(segment =>
  695. segment.includes('smoke') ||
  696. segment.includes('sample') ||
  697. segment.includes('debug') ||
  698. segment === 'fixture' ||
  699. segment === 'fixtures' ||
  700. segment === '__tests__' ||
  701. segment === 'test' ||
  702. segment === 'tests' ||
  703. segment === 'tmp' ||
  704. segment.startsWith('tmp-') ||
  705. segment === 'temp' ||
  706. segment.startsWith('temp-')
  707. );
  708. }
  709. function readJsonArg(file) {
  710. if (!file) return null;
  711. const resolved = path.resolve(file);
  712. if (!fs.existsSync(resolved)) return null;
  713. return JSON.parse(fs.readFileSync(resolved, 'utf8').replace(/^\uFEFF/, ''));
  714. }
  715. function relIfInside(root, file) {
  716. if (!file) return '';
  717. const resolvedRoot = path.resolve(root);
  718. const resolvedFile = path.resolve(file);
  719. const relative = path.relative(resolvedRoot, resolvedFile);
  720. if (relative && (relative.startsWith('..') || path.isAbsolute(relative))) return resolvedFile.replace(/\\/g, '/');
  721. return relative.replace(/\\/g, '/');
  722. }
  723. function parseArgs(argv) {
  724. const args = {};
  725. for (let index = 0; index < argv.length; index += 1) {
  726. const raw = argv[index];
  727. if (!raw.startsWith('--')) continue;
  728. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  729. const next = argv[index + 1];
  730. if (!next || next.startsWith('--')) args[key] = true;
  731. else {
  732. args[key] = next;
  733. index += 1;
  734. }
  735. }
  736. return args;
  737. }
  738. function escapeCell(value) {
  739. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  740. }
  741. function withBom(text) {
  742. return `\uFEFF${text}`;
  743. }
  744. if (require.main === module) main();
  745. module.exports = {
  746. buildCompletionSummary,
  747. renderReport,
  748. getVideoAbMissingRequirements,
  749. getVideoAbPreflightMissingRequirements,
  750. formatVideoAbEvidence,
  751. isRealVideoAbProof,
  752. bestHistoryAudit,
  753. bestCustomerEffectSummary,
  754. bestOvernightAggregate,
  755. isClosableOvernightAggregate
  756. };