proof-gap-closure-audit.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const OUTPUTS = path.join(ROOT, 'outputs');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputDir = path.resolve(args.output || process.env.TIHAO_PROOF_GAP_CLOSURE_OUTPUT || path.join(OUTPUTS, `proof-gap-closure-${Date.now()}`));
  9. const overnightFile = args.overnight || process.env.TIHAO_OVERNIGHT_AGGREGATE || bestOvernightAggregate(OUTPUTS) || latestFile(OUTPUTS, 'aggregate-summary.json');
  10. const historyFile = args.historyAudit || process.env.TIHAO_HISTORY_AUDIT || bestHistoryAudit(OUTPUTS) || latestFile(OUTPUTS, 'historical-dataset-audit.json');
  11. const customerEffectFile = args.customerEffect || process.env.TIHAO_CUSTOMER_EFFECT_SUMMARY || bestCustomerEffectSummary(OUTPUTS, historyFile, ROOT) || latestFile(OUTPUTS, 'customer-effect-summary.json');
  12. const videoAbFile = args.videoAb || process.env.TIHAO_VIDEO_AB_SUMMARY || latestFile(OUTPUTS, 'video-hit-rate-summary.json');
  13. const videoAbPreflightFile = args.videoAbPreflight || process.env.TIHAO_VIDEO_AB_PREFLIGHT_SUMMARY || latestFile(OUTPUTS, 'video-hit-rate-preflight-summary.json');
  14. const materialBridgeFile = args.materialBridge || process.env.TIHAO_MATERIAL_BRIDGE_SUMMARY || latestFile(OUTPUTS, 'local-seed-to-intake-worklist-summary.json');
  15. const summary = buildClosureSummary({
  16. root: ROOT,
  17. history: readJsonArg(historyFile),
  18. intakeReadiness: readJsonArg(args.intakeReadiness || process.env.TIHAO_INTAKE_READINESS_SUMMARY || latestFile(OUTPUTS, 'intake-readiness-summary.json')),
  19. videoReadiness: readJsonArg(args.videoReadiness || process.env.TIHAO_VIDEO_RESOURCE_READINESS_SUMMARY || latestFile(OUTPUTS, 'video-resource-readiness-summary.json')),
  20. videoAb: readJsonArg(videoAbFile),
  21. videoAbPreflight: readJsonArg(videoAbPreflightFile),
  22. overnight: readJsonArg(overnightFile),
  23. customerEffect: readJsonArg(customerEffectFile),
  24. materialBridge: readJsonArg(materialBridgeFile),
  25. sourceFiles: {
  26. overnight: relIfInside(ROOT, overnightFile),
  27. history: relIfInside(ROOT, historyFile),
  28. customerEffect: relIfInside(ROOT, customerEffectFile),
  29. videoAb: relIfInside(ROOT, videoAbFile),
  30. videoAbPreflight: relIfInside(ROOT, videoAbPreflightFile),
  31. materialBridge: relIfInside(ROOT, materialBridgeFile)
  32. }
  33. });
  34. fs.mkdirSync(outputDir, { recursive: true });
  35. const jsonPath = path.join(outputDir, 'proof-gap-closure-summary.json');
  36. const reportPath = path.join(outputDir, 'proof-gap-closure-report.md');
  37. fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
  38. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  39. console.log(JSON.stringify({
  40. outputDir,
  41. json: jsonPath,
  42. report: reportPath,
  43. complete: summary.complete,
  44. closedCount: summary.closedCount,
  45. openCount: summary.openCount
  46. }, null, 2));
  47. if ((args.strict || process.env.TIHAO_PROOF_GAP_CLOSURE_STRICT === 'true') && !summary.complete) {
  48. process.exitCode = 2;
  49. }
  50. }
  51. function buildClosureSummary({ root, history, intakeReadiness, videoReadiness, videoAb, videoAbPreflight, overnight, customerEffect, materialBridge, sourceFiles = {} }) {
  52. const intakeMissingRequirements = getIntakeMissingRequirements(intakeReadiness);
  53. const historyMissingRequirements = getHistoryMissingRequirements(history);
  54. const videoResourceMissingRequirements = getVideoResourceMissingRequirements(videoReadiness);
  55. const videoAbMissingRequirements = getVideoAbMissingRequirements(videoAb, videoAbPreflight);
  56. const customerEffectMissingRequirements = getCustomerEffectMissingRequirements(customerEffect, history, sourceFiles, root);
  57. const materialBridgeByGap = buildMaterialBridgeByGap(materialBridge);
  58. const rows = [
  59. closeRow({
  60. id: 'intake-readiness',
  61. title: '真实材料 Intake Readiness 预审',
  62. closed: intakeMissingRequirements.length === 0,
  63. evidence: intakeReadiness ? `overallReady=${Boolean(intakeReadiness.acceptance?.overallReady)}, failureCount=${intakeReadiness.failureCount ?? 'unknown'}, history=${Boolean(intakeReadiness.acceptance?.historyReady)}, video=${Boolean(intakeReadiness.acceptance?.videoReady)}, review=${Boolean(intakeReadiness.acceptance?.reviewMetricsReady)}, customerEffect=${Boolean(intakeReadiness.acceptance?.customerEffectReady)}` : 'missing intake-readiness-summary.json',
  64. required: 'intake:readiness 必须显示 overallReady=true 且 failureCount=0。',
  65. command: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest',
  66. expectedArtifact: 'outputs/intake-readiness-latest/intake-readiness-summary.json',
  67. missingProofRequirements: intakeMissingRequirements
  68. }),
  69. closeRow({
  70. id: 'historical-dataset',
  71. title: '真实历史 Brief 数据集',
  72. closed: historyMissingRequirements.length === 0,
  73. evidence: history ? `briefCount=${history.briefCount || 0}, readyForCustomerEffectProof=${Boolean(history.acceptance?.readyForCustomerEffectProof)}` : 'missing historical-dataset-audit.json',
  74. required: 'history:audit 必须显示 readyForCustomerEffectProof=true。',
  75. command: 'npm run history:from-csv -- --input <历史数据CSV> --output <history-dataset目录>;npm run history:audit -- --input <history-dataset目录> --output <历史审计输出目录> --strict',
  76. expectedArtifact: '<历史审计输出目录>/historical-dataset-audit.json',
  77. missingProofRequirements: historyMissingRequirements,
  78. materialBridge: materialBridgeByGap['historical-dataset']
  79. }),
  80. closeRow({
  81. id: 'video-real-candidate',
  82. title: '真实候选视频资源',
  83. closed: videoResourceMissingRequirements.length === 0,
  84. evidence: videoReadiness ? `realReference=${videoReadiness.counts?.realReferenceRows || 0}, realCandidate=${videoReadiness.counts?.realCandidateRows || 0}, failureCount=${videoReadiness.failureCount ?? 'unknown'}` : 'missing video-resource-readiness-summary.json',
  85. required: 'video:resource-readiness 必须 readyForVideoAbPreflight=true 且 failureCount=0。',
  86. command: 'npm run video:resource-readiness -- --input <video-resource-template.csv> --output <视频资源审计输出目录> --strict',
  87. expectedArtifact: '<视频资源审计输出目录>/video-resource-readiness-summary.json',
  88. missingProofRequirements: videoResourceMissingRequirements
  89. }),
  90. closeRow({
  91. id: 'video-ab-live-proof',
  92. title: '真实视频 A/B live proof',
  93. closed: videoAbMissingRequirements.length === 0,
  94. evidence: formatVideoAbEvidence(videoAb, videoAbPreflight),
  95. required: 'acceptance:video-ab 必须在真实 provider 和真实视频资源下通过。',
  96. command: 'npm run acceptance:video-ab',
  97. expectedArtifact: '<视频A/B输出目录>/video-hit-rate-summary.json',
  98. missingProofRequirements: videoAbMissingRequirements,
  99. materialBridge: materialBridgeByGap['video-ab-live-proof']
  100. }),
  101. closeRow({
  102. id: 'live-provider-overnight-proof',
  103. title: '真实 live/provider 长跑证明',
  104. closed: isClosableOvernightAggregate(overnight),
  105. evidence: overnight ? `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'}` : 'missing aggregate-summary.json',
  106. required: 'overnight aggregate 必须 liveEnabled=true、overallPass=true、failureCount=0、failedGateCount=0。',
  107. command: 'npm run longrun:readiness;真实 provider 就绪后运行 npm run overnight:quality',
  108. expectedArtifact: '<overnight输出目录>/aggregate-summary.json',
  109. missingProofRequirements: getOvernightMissingRequirements(overnight)
  110. }),
  111. closeRow({
  112. id: 'manual-review-and-customer-effect',
  113. title: '商务复核和客户效果证明',
  114. closed: customerEffectMissingRequirements.length === 0,
  115. evidence: customerEffect ? `overallPass=${Boolean(customerEffect.acceptance?.overallPass)}, customerSelectedRate=${customerEffect.customerSelectedRate ?? 'unknown'}, referenceCustomerSelectedRate=${customerEffect.referenceCustomerSelectedRate ?? 'unknown'}, manualSupplementReductionRate=${customerEffect.manualSupplementReductionRate ?? 'unknown'}, historyAuditPath=${customerEffect.historyAuditPath || 'missing'}` : 'missing customer-effect-summary.json',
  116. required: 'customer-effect:audit 必须 overallPass=true,并绑定当前通过的 history:audit、客户选择、参考链路和人工补号量。',
  117. command: 'npm run review:metrics -- --input <已标注CSV> --output <复核指标输出目录> --strict;npm run customer-effect:audit -- --review-csv <已标注CSV> --history-audit <historical-dataset-audit.json> --current-manual-supplement-count <本轮人工补号量> --output <客户效果输出目录> --strict',
  118. expectedArtifact: '<客户效果输出目录>/customer-effect-summary.json',
  119. missingProofRequirements: customerEffectMissingRequirements,
  120. materialBridge: materialBridgeByGap['manual-review-and-customer-effect']
  121. })
  122. ];
  123. const precheckRows = rows.filter(row => row.id === 'intake-readiness');
  124. const businessRows = rows.filter(row => row.id !== 'intake-readiness');
  125. const businessClosedCount = businessRows.filter(row => row.status === 'closed').length;
  126. const businessOpenCount = businessRows.length - businessClosedCount;
  127. const precheckClosedCount = precheckRows.filter(row => row.status === 'closed').length;
  128. const precheckOpenCount = precheckRows.length - precheckClosedCount;
  129. return {
  130. generatedAt: new Date().toISOString(),
  131. root,
  132. sourceFiles,
  133. complete: businessOpenCount === 0,
  134. intakeReady: precheckOpenCount === 0,
  135. closedCount: businessClosedCount,
  136. openCount: businessOpenCount,
  137. businessClosedCount,
  138. businessOpenCount,
  139. totalRows: rows.length,
  140. businessGapCount: businessRows.length,
  141. precheckRowCount: precheckRows.length,
  142. precheckClosedCount,
  143. precheckOpenCount,
  144. videoAbPreflightReadyForVideoAb: Boolean(videoAbPreflight?.readyForVideoAb),
  145. videoAbPreflightFailureCount: videoAbPreflight ? Number(videoAbPreflight.failureCount ?? 0) : null,
  146. countSemantics: {
  147. openCount: 'business_gap_open_count_excludes_precheck_rows',
  148. closedCount: 'business_gap_closed_count_excludes_precheck_rows',
  149. businessOpenCount: 'same_as_openCount',
  150. businessClosedCount: 'same_as_closedCount',
  151. precheckOpenCount: 'precheck_rows_reported_separately'
  152. },
  153. rows,
  154. guardrails: [
  155. 'closure 审计只承认真实审计产物,不承认 sample、smoke、模板或 provider fallback。',
  156. 'intake readiness 是补证预审,不计入业务缺口 openCount;但未通过时会提示模板仍是半成品。',
  157. '任一缺口为 open 时,不得宣布长期优化目标完成。',
  158. '不得在任何输入、报告或日志中写入 sessionToken、Authorization、模型 token 或 npm token。'
  159. ]
  160. };
  161. }
  162. function isRealVideoAbProof(videoAb) {
  163. return getVideoAbMissingRequirements(videoAb).length === 0;
  164. }
  165. function buildMaterialBridgeByGap(materialBridge) {
  166. const result = {};
  167. for (const item of materialBridge?.proofGapMaterialBridge || []) {
  168. if (!item || !item.id) continue;
  169. result[item.id] = {
  170. materialFound: Boolean(item.materialFound),
  171. connectedDraft: item.connectedDraft || '',
  172. draftRowCount: item.draftRowCount ?? null,
  173. referenceSeedRowCount: item.referenceSeedRowCount ?? null,
  174. videoCandidateSeedRowCount: item.videoCandidateSeedRowCount ?? null,
  175. canCloseProofGap: Boolean(item.canCloseProofGap),
  176. whyNotClosed: Array.isArray(item.whyNotClosed) ? item.whyNotClosed : []
  177. };
  178. }
  179. return result;
  180. }
  181. function getVideoAbMissingRequirements(videoAb, videoAbPreflight = null) {
  182. const missing = [];
  183. if (!videoAb) {
  184. missing.push('missing video-hit-rate-summary.json');
  185. } else {
  186. if (!videoAb.acceptance?.passed) missing.push('acceptance.passed=true');
  187. const context = videoAb.proofContext || {};
  188. if (context.mode !== 'live') missing.push('proofContext.mode=live');
  189. if (context.collectionMode !== 'live') missing.push('proofContext.collectionMode=live');
  190. if (context.generatedBy !== 'acceptance:video-ab') missing.push('proofContext.generatedBy=acceptance:video-ab');
  191. if (context.requiresRuntimeCredential !== true) missing.push('proofContext.requiresRuntimeCredential=true');
  192. if (context.requiresVocSocialProvider !== true) missing.push('proofContext.requiresVocSocialProvider=true');
  193. if (context.requiresVideoAnalysisProvider !== true) missing.push('proofContext.requiresVideoAnalysisProvider=true');
  194. if (videoAb.enhanced?.provider?.evidence?.providerStatus !== 'ok') missing.push('enhanced.provider.evidence.providerStatus=ok');
  195. }
  196. if (missing.length) {
  197. missing.push(...getVideoAbPreflightMissingRequirements(videoAbPreflight));
  198. }
  199. return uniqueList(missing);
  200. }
  201. function getVideoAbPreflightMissingRequirements(videoAbPreflight) {
  202. if (!videoAbPreflight) return [];
  203. const missing = [];
  204. if (videoAbPreflight.proofLevel !== 'not_business_proof') missing.push('videoAbPreflight.proofLevel=not_business_proof');
  205. if (videoAbPreflight.canCloseProofGap !== false) missing.push('videoAbPreflight.canCloseProofGap=false');
  206. if (videoAbPreflight.proofContext?.generatedBy !== 'acceptance:video-ab-preflight') missing.push('videoAbPreflight.proofContext.generatedBy=acceptance:video-ab-preflight');
  207. if (videoAbPreflight.readyForVideoAb !== true) {
  208. missing.push('videoAbPreflight.readyForVideoAb=true');
  209. for (const requirement of videoAbPreflight.missingProofRequirements || []) {
  210. missing.push(`videoAbPreflight.${requirement}`);
  211. }
  212. }
  213. return missing;
  214. }
  215. function formatVideoAbEvidence(videoAb, videoAbPreflight) {
  216. const liveEvidence = videoAb
  217. ? `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'}, strongDelta=${videoAb.delta?.strong ?? 'unknown'}, evidenceDelta=${videoAb.delta?.evidenceCards ?? 'unknown'}`
  218. : 'missing video-hit-rate-summary.json';
  219. if (!videoAbPreflight || getVideoAbMissingRequirements(videoAb).length === 0) return liveEvidence;
  220. const runtime = videoAbPreflight.runtime || {};
  221. const preflightEvidence = [
  222. `preflight.readyForVideoAb=${Boolean(videoAbPreflight.readyForVideoAb)}`,
  223. `preflight.failureCount=${videoAbPreflight.failureCount ?? 'unknown'}`,
  224. `preflight.generatedBy=${videoAbPreflight.proofContext?.generatedBy || 'unknown'}`,
  225. `preflight.proofLevel=${videoAbPreflight.proofLevel || 'unknown'}`,
  226. `preflight.canCloseProofGap=${Boolean(videoAbPreflight.canCloseProofGap)}`,
  227. `runtimeCredentialPresent=${Boolean(runtime.runtimeCredentialPresent)}`,
  228. `companyResolved=${Boolean(runtime.companyResolved)}`,
  229. `vocSocialProviderTokenPresent=${Boolean(runtime.vocSocialProviderTokenPresent)}`,
  230. `videoAnalysisTokenPresent=${Boolean(runtime.videoAnalysisTokenPresent)}`
  231. ].join(', ');
  232. return `${liveEvidence}; ${preflightEvidence}`;
  233. }
  234. function getIntakeMissingRequirements(intakeReadiness) {
  235. const missing = [];
  236. if (!intakeReadiness) return ['missing intake-readiness-summary.json'];
  237. const acceptance = intakeReadiness.acceptance || {};
  238. if (acceptance.overallReady !== true) missing.push('acceptance.overallReady=true');
  239. if (Number(intakeReadiness.failureCount || 0) !== 0) missing.push('failureCount=0');
  240. if (acceptance.historyReady !== true) missing.push('acceptance.historyReady=true');
  241. if (acceptance.videoReady !== true) missing.push('acceptance.videoReady=true');
  242. if (acceptance.reviewMetricsReady !== true) missing.push('acceptance.reviewMetricsReady=true');
  243. if (acceptance.customerEffectReady !== true) missing.push('acceptance.customerEffectReady=true');
  244. return missing;
  245. }
  246. function getHistoryMissingRequirements(history) {
  247. const missing = [];
  248. if (!history) return ['missing historical-dataset-audit.json'];
  249. const acceptance = history.acceptance || {};
  250. if (history.acceptance?.readyForCustomerEffectProof !== true) missing.push('acceptance.readyForCustomerEffectProof=true');
  251. if (Number(history.briefCount || 0) < 5) missing.push('briefCount>=5');
  252. if (acceptance.parseOk !== true) missing.push('acceptance.parseOk=true');
  253. if (acceptance.minBriefsMet !== true) missing.push('acceptance.minBriefsMet=true');
  254. if (acceptance.allHaveBriefText !== true) missing.push('acceptance.allHaveBriefText=true');
  255. if (acceptance.allHaveManualFinalList !== true) missing.push('acceptance.allHaveManualFinalList=true');
  256. if (acceptance.allHaveCustomerDecision !== true) missing.push('acceptance.allHaveCustomerDecision=true');
  257. if (acceptance.allHaveFeedbackReason !== true) missing.push('acceptance.allHaveFeedbackReason=true');
  258. if (acceptance.allHaveManualSupplementBaseline !== true) missing.push('acceptance.allHaveManualSupplementBaseline=true');
  259. if (acceptance.categoryCoverageMet !== true) missing.push('acceptance.categoryCoverageMet=true');
  260. if (Number(history.missingCriticalCount || 0) !== 0) missing.push('missingCriticalCount=0');
  261. if (!Array.isArray(history.items) || history.items.length < 5) missing.push('items.length>=5');
  262. if (Number(history.withManualFinalList || 0) < 5) missing.push('withManualFinalList>=5');
  263. if (Number(history.withCustomerDecision || 0) < 5) missing.push('withCustomerDecision>=5');
  264. if (Number(history.withRejectionReason || 0) < 1) missing.push('withRejectionReason>=1');
  265. if (Number(history.withManualSupplementBaseline || 0) < 5) missing.push('withManualSupplementBaseline>=5');
  266. return missing;
  267. }
  268. function getVideoResourceMissingRequirements(videoReadiness) {
  269. const missing = [];
  270. if (!videoReadiness) return ['missing video-resource-readiness-summary.json'];
  271. if (videoReadiness.acceptance?.readyForVideoAbPreflight !== true) missing.push('acceptance.readyForVideoAbPreflight=true');
  272. if (Number(videoReadiness.failureCount || 0) !== 0) missing.push('failureCount=0');
  273. if (Number(videoReadiness.counts?.realReferenceRows || 0) < 1) missing.push('counts.realReferenceRows>=1');
  274. if (Number(videoReadiness.counts?.realCandidateRows || 0) < 1) missing.push('counts.realCandidateRows>=1');
  275. if (Number(videoReadiness.counts?.videoUrlRows || 0) < 1) missing.push('counts.videoUrlRows>=1');
  276. return missing;
  277. }
  278. function getOvernightMissingRequirements(overnight) {
  279. const missing = [];
  280. if (!overnight) return ['missing aggregate-summary.json'];
  281. if (overnight.manifest?.liveEnabled !== true) missing.push('manifest.liveEnabled=true');
  282. if (overnight.acceptance?.overallPass !== true) missing.push('acceptance.overallPass=true');
  283. if (Number(overnight.failureCount || 0) !== 0) missing.push('failureCount=0');
  284. if (Number(overnight.acceptance?.failedGateCount ?? overnight.failedGateCount ?? 0) !== 0) missing.push('acceptance.failedGateCount=0');
  285. if (overnight.acceptance?.tokenLeak === true) missing.push('acceptance.tokenLeak=false');
  286. return missing;
  287. }
  288. function getCustomerEffectMissingRequirements(customerEffect, history, sourceFiles = {}, root = process.cwd()) {
  289. const missing = [];
  290. if (!customerEffect) return ['missing customer-effect-summary.json'];
  291. const acceptance = customerEffect.acceptance || {};
  292. if (acceptance.overallPass !== true) missing.push('acceptance.overallPass=true');
  293. if (acceptance.reviewMetricsOverallUsable !== true) missing.push('acceptance.reviewMetricsOverallUsable=true');
  294. if (acceptance.customerSelectedRateMeasured !== true) missing.push('acceptance.customerSelectedRateMeasured=true');
  295. if (acceptance.customerSelectedRate30Pass !== true) missing.push('acceptance.customerSelectedRate30Pass=true');
  296. if (acceptance.referenceCustomerSelectedRateMeasured !== true) missing.push('acceptance.referenceCustomerSelectedRateMeasured=true');
  297. if (acceptance.referenceCustomerSelectedRate40Pass !== true) missing.push('acceptance.referenceCustomerSelectedRate40Pass=true');
  298. if (acceptance.historyReadyForCustomerEffectProof !== true) missing.push('acceptance.historyReadyForCustomerEffectProof=true');
  299. if (acceptance.manualSupplementBaselineAvailable !== true) missing.push('acceptance.manualSupplementBaselineAvailable=true');
  300. if (acceptance.currentManualSupplementAvailable !== true) missing.push('acceptance.currentManualSupplementAvailable=true');
  301. if (acceptance.manualSupplementReduction50Pass !== true) missing.push('acceptance.manualSupplementReduction50Pass=true');
  302. if (!customerEffect.reviewPath) missing.push('reviewPath=provided');
  303. if (!customerEffect.historyAuditPath) missing.push('historyAuditPath=provided');
  304. if (sourceFiles.history && customerEffect.historyAuditPath && !samePath(root, customerEffect.historyAuditPath, sourceFiles.history)) {
  305. missing.push('historyAuditPath=current-history-audit');
  306. }
  307. if (!history) {
  308. missing.push('currentHistoryAudit=present');
  309. } else {
  310. for (const requirement of getHistoryMissingRequirements(history)) {
  311. missing.push(`currentHistoryAudit.${requirement}`);
  312. }
  313. }
  314. if (!Number.isFinite(Number(customerEffect.customerSelectedRate))) missing.push('customerSelectedRate=number');
  315. if (!Number.isFinite(Number(customerEffect.referenceCustomerSelectedRate))) missing.push('referenceCustomerSelectedRate=number');
  316. if (!Number.isFinite(Number(customerEffect.manualSupplementReductionRate))) missing.push('manualSupplementReductionRate=number');
  317. if (customerEffect.manualSupplementBaseline?.available !== true) missing.push('manualSupplementBaseline.available=true');
  318. if (customerEffect.currentManualSupplement?.available !== true) missing.push('currentManualSupplement.available=true');
  319. return uniqueList([...(customerEffect.missingEvidence || []), ...missing]);
  320. }
  321. function closeRow({ id, title, closed, evidence, required, command, expectedArtifact, missingProofRequirements = [], materialBridge = null }) {
  322. const normalizedMissing = closed ? [] : uniqueList(missingProofRequirements);
  323. return {
  324. id,
  325. title,
  326. status: closed ? 'closed' : 'open',
  327. evidence,
  328. missingProofRequirements: normalizedMissing,
  329. required,
  330. command,
  331. expectedArtifact,
  332. ...(materialBridge ? { materialBridge } : {}),
  333. next: closed ? '保持证据链并纳入 evidence:index。' : `${command};产物:${expectedArtifact}`
  334. };
  335. }
  336. function uniqueList(values) {
  337. return Array.from(new Set((Array.isArray(values) ? values : []).map(value => String(value || '').trim()).filter(Boolean)));
  338. }
  339. function renderReport(summary) {
  340. return [
  341. '# 提号真实证明缺口关闭审计',
  342. '',
  343. `- 生成时间:${summary.generatedAt}`,
  344. `- 是否全部关闭:${summary.complete ? '是' : '否'}`,
  345. `- Intake 预审是否就绪:${summary.intakeReady ? '是' : '否'}`,
  346. `- 已关闭:${summary.closedCount}`,
  347. `- 未关闭:${summary.openCount}`,
  348. `- 业务证明缺口总数:${summary.businessGapCount ?? summary.openCount + summary.closedCount}`,
  349. `- 业务证明缺口已关闭:${summary.businessClosedCount ?? summary.closedCount}`,
  350. `- 业务证明缺口未关闭:${summary.businessOpenCount ?? summary.openCount}`,
  351. `- 预审行总数:${summary.precheckRowCount ?? 0}`,
  352. `- 预审未就绪行:${summary.precheckOpenCount ?? (summary.intakeReady ? 0 : 1)}`,
  353. '',
  354. '## 关闭状态',
  355. '',
  356. '| 缺口ID | 名称 | 状态 | 当前证据 | 缺失证明 | 关闭标准 | 验证命令 | 期望产物 | 下一步 |',
  357. '| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
  358. ...summary.rows.map(row => `| ${row.id} | ${row.title} | ${row.status} | ${escapeCell(row.evidence)} | ${escapeCell((row.missingProofRequirements || []).join(';') || '-')} | ${escapeCell(row.required)} | \`${escapeCell(row.command)}\` | ${escapeCell(row.expectedArtifact)} | ${escapeCell(row.next)} |`),
  359. '',
  360. '## 边界',
  361. '',
  362. ...summary.guardrails.map(item => `- ${item}`)
  363. ].join('\n');
  364. }
  365. function latestFile(outputsDir, fileName) {
  366. if (!fs.existsSync(outputsDir)) return '';
  367. return fs.readdirSync(outputsDir, { withFileTypes: true })
  368. .filter(entry => entry.isDirectory())
  369. .map(entry => path.join(outputsDir, entry.name, fileName))
  370. .filter(file => fs.existsSync(file))
  371. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || '';
  372. }
  373. function bestHistoryAudit(outputsDir) {
  374. return bestProofArtifact(outputsDir, 'historical-dataset-audit.json', {
  375. isClaimable: item => getHistoryMissingRequirements(item.json).length === 0
  376. });
  377. }
  378. function bestCustomerEffectSummary(outputsDir, historyFile = '', root = process.cwd()) {
  379. const history = readJsonArg(historyFile);
  380. const sourceFiles = { history: relIfInside(root, historyFile) };
  381. return bestProofArtifact(outputsDir, 'customer-effect-summary.json', {
  382. isClaimable: item => getCustomerEffectMissingRequirements(item.json, history, sourceFiles, root).length === 0,
  383. score: item => {
  384. if (!historyFile || !item.json?.historyAuditPath) return 0;
  385. return samePath(root, item.json.historyAuditPath, historyFile) ? 2 : 0;
  386. }
  387. });
  388. }
  389. function bestProofArtifact(outputsDir, fileName, { isClaimable = () => false, score = () => 0 } = {}) {
  390. const annotated = findFiles(outputsDir, fileName)
  391. .filter(file => isBusinessArtifactPath(outputsDir, file))
  392. .map(file => ({ file, json: readJsonArg(file), mtimeMs: fs.statSync(file).mtimeMs }))
  393. .filter(item => item.json);
  394. if (!annotated.length) return '';
  395. return annotated
  396. .sort((left, right) => {
  397. const rightClaimable = safePredicate(isClaimable, right) ? 1 : 0;
  398. const leftClaimable = safePredicate(isClaimable, left) ? 1 : 0;
  399. if (rightClaimable !== leftClaimable) return rightClaimable - leftClaimable;
  400. const rightScore = Number(score(right) || 0);
  401. const leftScore = Number(score(left) || 0);
  402. if (rightScore !== leftScore) return rightScore - leftScore;
  403. return right.mtimeMs - left.mtimeMs;
  404. })[0].file;
  405. }
  406. function safePredicate(predicate, item) {
  407. try {
  408. return Boolean(predicate(item));
  409. } catch (_error) {
  410. return false;
  411. }
  412. }
  413. function isBusinessArtifactPath(outputsDir, file) {
  414. const relative = path.relative(outputsDir, file).replace(/\\/g, '/').toLowerCase();
  415. const segments = relative.split('/').filter(Boolean);
  416. return !segments.some(segment =>
  417. segment.includes('smoke') ||
  418. segment.includes('sample') ||
  419. segment.includes('debug') ||
  420. segment === 'fixture' ||
  421. segment === 'fixtures' ||
  422. segment === '__tests__' ||
  423. segment === 'test' ||
  424. segment === 'tests' ||
  425. segment === 'tmp' ||
  426. segment.startsWith('tmp-') ||
  427. segment === 'temp' ||
  428. segment.startsWith('temp-')
  429. );
  430. }
  431. function bestOvernightAggregate(outputsDir) {
  432. const files = findFiles(outputsDir, 'aggregate-summary.json');
  433. const annotated = files
  434. .map(file => ({ file, json: readJsonArg(file), mtimeMs: fs.statSync(file).mtimeMs }))
  435. .filter(item => item.json);
  436. const closable = annotated
  437. .filter(item => isClosableOvernightAggregate(item.json))
  438. .sort((a, b) => b.mtimeMs - a.mtimeMs);
  439. if (closable.length) return closable[0].file;
  440. return annotated.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.file || '';
  441. }
  442. function findFiles(dir, fileName) {
  443. if (!fs.existsSync(dir)) return [];
  444. const result = [];
  445. const stack = [dir];
  446. while (stack.length) {
  447. const current = stack.pop();
  448. for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
  449. const full = path.join(current, entry.name);
  450. if (entry.isDirectory()) stack.push(full);
  451. else if (entry.isFile() && entry.name === fileName) result.push(full);
  452. }
  453. }
  454. return result;
  455. }
  456. function isClosableOvernightAggregate(aggregate) {
  457. return Boolean(
  458. aggregate?.manifest?.liveEnabled === true &&
  459. aggregate?.acceptance?.overallPass === true &&
  460. Number(aggregate?.failureCount || 0) === 0 &&
  461. Number(aggregate?.acceptance?.failedGateCount ?? aggregate?.failedGateCount ?? 0) === 0 &&
  462. aggregate?.acceptance?.tokenLeak !== true
  463. );
  464. }
  465. function readJsonArg(file) {
  466. if (!file) return null;
  467. const resolved = path.resolve(file);
  468. if (!fs.existsSync(resolved)) return null;
  469. return JSON.parse(fs.readFileSync(resolved, 'utf8').replace(/^\uFEFF/, ''));
  470. }
  471. function relIfInside(root, file) {
  472. if (!file) return '';
  473. const resolvedRoot = path.resolve(root);
  474. const resolvedFile = path.resolve(file);
  475. const relative = path.relative(resolvedRoot, resolvedFile);
  476. if (relative && (relative.startsWith('..') || path.isAbsolute(relative))) return resolvedFile.replace(/\\/g, '/');
  477. return relative.replace(/\\/g, '/');
  478. }
  479. function samePath(root, left, right) {
  480. const normalize = value => {
  481. const resolved = path.resolve(root || process.cwd(), value || '');
  482. return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
  483. };
  484. return normalize(left) === normalize(right);
  485. }
  486. function parseArgs(argv) {
  487. const args = {};
  488. for (let index = 0; index < argv.length; index += 1) {
  489. const raw = argv[index];
  490. if (!raw.startsWith('--')) continue;
  491. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  492. const next = argv[index + 1];
  493. if (!next || next.startsWith('--')) args[key] = true;
  494. else {
  495. args[key] = next;
  496. index += 1;
  497. }
  498. }
  499. return args;
  500. }
  501. function escapeCell(value) {
  502. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  503. }
  504. function withBom(text) {
  505. return `\uFEFF${text}`;
  506. }
  507. if (require.main === module) main();
  508. module.exports = {
  509. buildClosureSummary,
  510. renderReport,
  511. getVideoAbMissingRequirements,
  512. getIntakeMissingRequirements,
  513. getHistoryMissingRequirements,
  514. getVideoResourceMissingRequirements,
  515. getOvernightMissingRequirements,
  516. getCustomerEffectMissingRequirements,
  517. isRealVideoAbProof,
  518. getVideoAbPreflightMissingRequirements,
  519. formatVideoAbEvidence,
  520. bestHistoryAudit,
  521. bestCustomerEffectSummary,
  522. bestOvernightAggregate,
  523. isClosableOvernightAggregate
  524. };