proof-gap-closure-smoke.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const {
  6. buildClosureSummary,
  7. renderReport,
  8. bestHistoryAudit,
  9. bestCustomerEffectSummary,
  10. bestOvernightAggregate,
  11. isClosableOvernightAggregate
  12. } = require('./proof-gap-closure-audit');
  13. function main() {
  14. const historyAuditPath = path.join('outputs', 'history-audit-latest', 'historical-dataset-audit.json');
  15. const staleHistoryAuditPath = path.join('outputs', 'stale-history-audit', 'historical-dataset-audit.json');
  16. const sourceFiles = { history: historyAuditPath };
  17. const passingHistory = passingHistoryAudit();
  18. const open = buildClosureSummary({
  19. root: process.cwd(),
  20. intakeReadiness: { acceptance: { overallReady: false, historyReady: false, videoReady: false, reviewMetricsReady: false, customerEffectReady: false }, failureCount: 9 },
  21. history: null,
  22. videoReadiness: { acceptance: { readyForVideoAbPreflight: false }, counts: { realReferenceRows: 1, realCandidateRows: 0 }, failureCount: 1 },
  23. videoAb: { acceptance: { passed: false }, delta: { strong: 1, evidenceCards: 2 } },
  24. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: false, failedGateCount: 1 }, failureCount: 0 },
  25. customerEffect: { acceptance: { overallPass: false }, customerSelectedRate: 0.2 }
  26. });
  27. assert(!open.complete, 'open summary should not be complete');
  28. assert(open.openCount === 5, `openCount should be 5, got ${open.openCount}`);
  29. assert(open.businessOpenCount === 5, `businessOpenCount should be 5, got ${open.businessOpenCount}`);
  30. assert(open.openCount === open.businessOpenCount, 'openCount should remain a business-open alias for compatibility');
  31. assert(open.businessClosedCount === 0, `businessClosedCount should be 0, got ${open.businessClosedCount}`);
  32. assert(open.businessGapCount === 5, `businessGapCount should be 5, got ${open.businessGapCount}`);
  33. assert(open.precheckRowCount === 1, `precheckRowCount should be 1, got ${open.precheckRowCount}`);
  34. assert(open.precheckOpenCount === 1, `precheckOpenCount should be 1, got ${open.precheckOpenCount}`);
  35. assert(open.totalRows === 6, `totalRows should include intake precheck row, got ${open.totalRows}`);
  36. assert(open.countSemantics?.openCount === 'business_gap_open_count_excludes_precheck_rows', 'summary should document openCount count semantics');
  37. assert(open.intakeReady === false, 'open summary should record intake readiness as false');
  38. assert(open.rows.every(row => row.status === 'open'), 'all open fixture rows should be open');
  39. assert(open.rows.some(row => row.id === 'intake-readiness'), 'summary should include intake readiness row');
  40. assert(open.rows.every(row => row.command && row.expectedArtifact), 'all rows should include command and expected artifact');
  41. assert(open.rows.every(row => Array.isArray(row.missingProofRequirements) && row.missingProofRequirements.length >= 1), 'all open fixture rows should explain missing proof requirements');
  42. assert(open.rows.find(row => row.id === 'intake-readiness').missingProofRequirements.includes('acceptance.overallReady=true'), 'intake row should explain missing overall readiness');
  43. assert(open.rows.find(row => row.id === 'historical-dataset').missingProofRequirements.includes('missing historical-dataset-audit.json'), 'history row should explain missing history audit');
  44. assert(open.rows.find(row => row.id === 'video-real-candidate').missingProofRequirements.includes('counts.realCandidateRows>=1'), 'video resource row should explain missing real candidate rows');
  45. assert(open.rows.find(row => row.id === 'live-provider-overnight-proof').missingProofRequirements.includes('acceptance.overallPass=true'), 'overnight row should explain missing overall pass');
  46. assert(open.rows.find(row => row.id === 'manual-review-and-customer-effect').missingProofRequirements.includes('acceptance.overallPass=true'), 'customer effect row should explain missing customer effect pass');
  47. const closed = buildClosureSummary({
  48. root: process.cwd(),
  49. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  50. history: passingHistory,
  51. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  52. videoAb: {
  53. acceptance: { passed: true },
  54. proofContext: {
  55. mode: 'live',
  56. collectionMode: 'live',
  57. generatedBy: 'acceptance:video-ab',
  58. requiresRuntimeCredential: true,
  59. requiresVocSocialProvider: true,
  60. requiresVideoAnalysisProvider: true
  61. },
  62. enhanced: { provider: { evidence: { providerStatus: 'ok' } } },
  63. delta: { strong: 1, evidenceCards: 3 }
  64. },
  65. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  66. customerEffect: passingCustomerEffect(historyAuditPath),
  67. sourceFiles
  68. });
  69. assert(closed.complete, 'closed summary should be complete');
  70. assert(closed.closedCount === 5, `closedCount should be 5, got ${closed.closedCount}`);
  71. assert(closed.businessClosedCount === 5, `businessClosedCount should be 5, got ${closed.businessClosedCount}`);
  72. assert(closed.closedCount === closed.businessClosedCount, 'closedCount should remain a business-closed alias for compatibility');
  73. assert(closed.businessOpenCount === 0, `businessOpenCount should be 0, got ${closed.businessOpenCount}`);
  74. assert(closed.precheckOpenCount === 0, `precheckOpenCount should be 0, got ${closed.precheckOpenCount}`);
  75. assert(closed.precheckClosedCount === 1, `precheckClosedCount should be 1, got ${closed.precheckClosedCount}`);
  76. assert(closed.intakeReady === true, 'closed summary should record intake readiness as true');
  77. assert(closed.rows.every(row => Array.isArray(row.missingProofRequirements) && row.missingProofRequirements.length === 0), 'closed rows should not keep stale missing proof requirements');
  78. const report = renderReport(closed);
  79. assert(report.includes('是否全部关闭:是'), 'report should render completion');
  80. assert(report.includes('Intake 预审是否就绪:是'), 'report should render intake readiness');
  81. assert(report.includes('业务证明缺口未关闭:0'), 'report should render business open proof gap count');
  82. assert(report.includes('预审未就绪行:0'), 'report should render precheck open row count separately');
  83. assert(report.includes('缺失证明'), 'report should render missing proof column');
  84. assert(report.includes('验证命令'), 'report should render command column');
  85. assert(report.includes('期望产物'), 'report should render expected artifact column');
  86. assert(!/sk-[A-Za-z0-9_-]{8,}/.test(report), 'report should not include secret-like values');
  87. const thinIntake = buildClosureSummary({
  88. root: process.cwd(),
  89. intakeReadiness: { acceptance: { overallReady: true, historyReady: false, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  90. history: passingHistory,
  91. videoReadiness: passingVideoReadiness(),
  92. videoAb: passingVideoAb(),
  93. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  94. customerEffect: passingCustomerEffect(historyAuditPath),
  95. sourceFiles
  96. });
  97. const thinIntakeRow = thinIntake.rows.find(row => row.id === 'intake-readiness');
  98. assert(thinIntakeRow.status === 'open', 'intake with top-level overallReady but missing child readiness should remain open');
  99. assert(thinIntakeRow.missingProofRequirements.includes('acceptance.historyReady=true'), 'thin intake should require history readiness');
  100. const thinHistory = buildClosureSummary({
  101. root: process.cwd(),
  102. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  103. history: { briefCount: 3, acceptance: { readyForCustomerEffectProof: true } },
  104. videoReadiness: passingVideoReadiness(),
  105. videoAb: passingVideoAb(),
  106. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  107. customerEffect: passingCustomerEffect(historyAuditPath),
  108. sourceFiles
  109. });
  110. const thinHistoryRow = thinHistory.rows.find(row => row.id === 'historical-dataset');
  111. assert(thinHistoryRow.status === 'open', 'history audit with fewer than 5 briefs should remain open');
  112. assert(thinHistoryRow.missingProofRequirements.includes('briefCount>=5'), 'thin history should require at least 5 briefs');
  113. const thinTopLevelHistory = buildClosureSummary({
  114. root: process.cwd(),
  115. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  116. history: { briefCount: 5, acceptance: { readyForCustomerEffectProof: true } },
  117. videoReadiness: passingVideoReadiness(),
  118. videoAb: passingVideoAb(),
  119. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  120. customerEffect: passingCustomerEffect(historyAuditPath),
  121. sourceFiles
  122. });
  123. const thinTopLevelHistoryRow = thinTopLevelHistory.rows.find(row => row.id === 'historical-dataset');
  124. assert(thinTopLevelHistoryRow.status === 'open', 'history audit with only top-level ready flag should remain open');
  125. assert(thinTopLevelHistoryRow.missingProofRequirements.includes('acceptance.allHaveManualFinalList=true'), 'thin history should require manual final-list coverage');
  126. assert(thinTopLevelHistoryRow.missingProofRequirements.includes('items.length>=5'), 'thin history should include auditable item details');
  127. const thinVideoResource = buildClosureSummary({
  128. root: process.cwd(),
  129. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  130. history: passingHistory,
  131. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1 }, failureCount: 0 },
  132. videoAb: passingVideoAb(),
  133. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  134. customerEffect: passingCustomerEffect(historyAuditPath),
  135. sourceFiles
  136. });
  137. const thinVideoResourceRow = thinVideoResource.rows.find(row => row.id === 'video-real-candidate');
  138. assert(thinVideoResourceRow.status === 'open', 'video readiness without video URL rows should remain open');
  139. assert(thinVideoResourceRow.missingProofRequirements.includes('counts.videoUrlRows>=1'), 'thin video readiness should require video URL rows');
  140. const thinCustomerEffect = buildClosureSummary({
  141. root: process.cwd(),
  142. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  143. history: passingHistory,
  144. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  145. videoAb: passingVideoAb(),
  146. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  147. customerEffect: { acceptance: { overallPass: true }, customerSelectedRate: 0.42, manualSupplementReductionRate: 0.5, historyAuditPath },
  148. sourceFiles
  149. });
  150. const thinCustomerEffectRow = thinCustomerEffect.rows.find(row => row.id === 'manual-review-and-customer-effect');
  151. assert(thinCustomerEffectRow.status === 'open', 'thin customer-effect summary should not close customer proof');
  152. assert(thinCustomerEffectRow.missingProofRequirements.includes('acceptance.reviewMetricsOverallUsable=true'), 'thin customer-effect summary should require review metrics usability');
  153. assert(thinCustomerEffectRow.missingProofRequirements.includes('referenceCustomerSelectedRate=number'), 'thin customer-effect summary should require reference customer selected rate');
  154. const mismatchedCustomerEffect = buildClosureSummary({
  155. root: process.cwd(),
  156. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  157. history: passingHistory,
  158. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  159. videoAb: passingVideoAb(),
  160. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  161. customerEffect: passingCustomerEffect(staleHistoryAuditPath),
  162. sourceFiles
  163. });
  164. const mismatchedCustomerEffectRow = mismatchedCustomerEffect.rows.find(row => row.id === 'manual-review-and-customer-effect');
  165. assert(mismatchedCustomerEffectRow.status === 'open', 'customer-effect summary bound to stale history audit should stay open');
  166. assert(mismatchedCustomerEffectRow.missingProofRequirements.includes('historyAuditPath=current-history-audit'), 'customer-effect summary should bind to current history audit path');
  167. const fakeVideo = buildClosureSummary({
  168. root: process.cwd(),
  169. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  170. history: passingHistory,
  171. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  172. videoAb: { acceptance: { passed: true }, delta: { strong: 1, evidenceCards: 3 } },
  173. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  174. customerEffect: passingCustomerEffect(historyAuditPath),
  175. sourceFiles
  176. });
  177. assert(!fakeVideo.complete, 'passed video A/B without live proofContext should not close all proof gaps');
  178. const fakeVideoRow = fakeVideo.rows.find(row => row.id === 'video-ab-live-proof');
  179. assert(fakeVideoRow.status === 'open', 'video A/B without live proofContext should remain open');
  180. assert(fakeVideoRow.missingProofRequirements.includes('proofContext.mode=live'), 'video A/B row should explain missing live mode');
  181. assert(fakeVideoRow.missingProofRequirements.includes('proofContext.generatedBy=acceptance:video-ab'), 'video A/B row should explain missing generatedBy');
  182. const preflightVideo = buildClosureSummary({
  183. root: process.cwd(),
  184. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  185. history: passingHistory,
  186. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  187. videoAb: { acceptance: { passed: true }, enhanced: { provider: { evidence: { providerStatus: 'ok' } } }, delta: { strong: 1, evidenceCards: 3 } },
  188. videoAbPreflight: failedVideoAbPreflight(),
  189. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  190. customerEffect: passingCustomerEffect(historyAuditPath),
  191. sourceFiles
  192. });
  193. assert(!preflightVideo.complete, 'video A/B preflight failure context should not close live proof');
  194. assert(preflightVideo.videoAbPreflightReadyForVideoAb === false, 'summary should expose video A/B preflight readiness');
  195. assert(preflightVideo.videoAbPreflightFailureCount === 4, 'summary should expose video A/B preflight failure count');
  196. const preflightVideoRow = preflightVideo.rows.find(row => row.id === 'video-ab-live-proof');
  197. assert(preflightVideoRow.status === 'open', 'video A/B row should remain open when only preflight failed');
  198. assert(preflightVideoRow.evidence.includes('preflight.readyForVideoAb=false'), 'video A/B evidence should include preflight readiness');
  199. assert(preflightVideoRow.evidence.includes('runtimeCredentialPresent=false'), 'video A/B evidence should include runtime credential presence only');
  200. assert(preflightVideoRow.missingProofRequirements.includes('videoAbPreflight.readyForVideoAb=true'), 'video A/B row should require preflight readiness before live proof can run');
  201. assert(preflightVideoRow.missingProofRequirements.includes('videoAbPreflight.company resolved for live run'), 'video A/B row should include preflight missing company context');
  202. const liveVideoWithStalePreflight = buildClosureSummary({
  203. root: process.cwd(),
  204. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  205. history: passingHistory,
  206. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  207. videoAb: passingVideoAb(),
  208. videoAbPreflight: failedVideoAbPreflight(),
  209. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0 }, failureCount: 0 },
  210. customerEffect: passingCustomerEffect(historyAuditPath),
  211. sourceFiles
  212. });
  213. assert(liveVideoWithStalePreflight.complete, 'passing live video A/B proof should not be blocked by an older preflight failure');
  214. const liveVideoRow = liveVideoWithStalePreflight.rows.find(row => row.id === 'video-ab-live-proof');
  215. assert(liveVideoRow.status === 'closed', 'passing live video A/B row should close');
  216. assert(!liveVideoRow.evidence.includes('preflight.readyForVideoAb=false'), 'closed live video proof should not foreground stale preflight failure context');
  217. const tokenLeak = buildClosureSummary({
  218. root: process.cwd(),
  219. intakeReadiness: { acceptance: { overallReady: true, historyReady: true, videoReady: true, reviewMetricsReady: true, customerEffectReady: true }, failureCount: 0 },
  220. history: passingHistory,
  221. videoReadiness: { acceptance: { readyForVideoAbPreflight: true }, counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 }, failureCount: 0 },
  222. videoAb: {
  223. acceptance: { passed: true },
  224. proofContext: {
  225. mode: 'live',
  226. collectionMode: 'live',
  227. generatedBy: 'acceptance:video-ab',
  228. requiresRuntimeCredential: true,
  229. requiresVocSocialProvider: true,
  230. requiresVideoAnalysisProvider: true
  231. },
  232. enhanced: { provider: { evidence: { providerStatus: 'ok' } } },
  233. delta: { strong: 1, evidenceCards: 3 }
  234. },
  235. overnight: { manifest: { liveEnabled: true }, acceptance: { overallPass: true, failedGateCount: 0, tokenLeak: true }, failureCount: 0 },
  236. customerEffect: passingCustomerEffect(historyAuditPath),
  237. sourceFiles
  238. });
  239. const tokenLeakRow = tokenLeak.rows.find(row => row.id === 'live-provider-overnight-proof');
  240. assert(tokenLeakRow.status === 'open', 'overnight aggregate with token leak should remain open');
  241. assert(tokenLeakRow.missingProofRequirements.includes('acceptance.tokenLeak=false'), 'overnight token leak should be listed as a missing requirement');
  242. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-proof-gap-'));
  243. try {
  244. const closableDir = path.join(tmp, 'older-closable');
  245. const badDir = path.join(tmp, 'newer-bad');
  246. fs.mkdirSync(closableDir, { recursive: true });
  247. fs.mkdirSync(badDir, { recursive: true });
  248. const closableFile = path.join(closableDir, 'aggregate-summary.json');
  249. const badFile = path.join(badDir, 'aggregate-summary.json');
  250. fs.writeFileSync(closableFile, JSON.stringify({
  251. manifest: { liveEnabled: true },
  252. acceptance: { overallPass: true, failedGateCount: 0, tokenLeak: false },
  253. failureCount: 0
  254. }), 'utf8');
  255. fs.writeFileSync(badFile, JSON.stringify({
  256. manifest: { liveEnabled: false },
  257. acceptance: { overallPass: false, failedGateCount: 1, tokenLeak: false },
  258. failureCount: 0
  259. }), 'utf8');
  260. fs.utimesSync(closableFile, new Date(Date.now() - 10000), new Date(Date.now() - 10000));
  261. fs.utimesSync(badFile, new Date(), new Date());
  262. const best = bestOvernightAggregate(tmp);
  263. assert(path.resolve(best) === path.resolve(closableFile), 'best overnight aggregate should prefer an older closable aggregate over a newer bad aggregate');
  264. assert(isClosableOvernightAggregate(JSON.parse(fs.readFileSync(best, 'utf8'))), 'selected overnight aggregate should be closable');
  265. } finally {
  266. fs.rmSync(tmp, { recursive: true, force: true });
  267. }
  268. const proofTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-proof-discovery-'));
  269. try {
  270. const realHistoryDir = path.join(proofTmp, 'pipeline-run', 'history-audit');
  271. const smokeHistoryDir = path.join(proofTmp, 'pipeline-smoke-run', 'history-audit');
  272. const newerFailingHistoryDir = path.join(proofTmp, 'latest-real-run', 'history-audit');
  273. fs.mkdirSync(realHistoryDir, { recursive: true });
  274. fs.mkdirSync(smokeHistoryDir, { recursive: true });
  275. fs.mkdirSync(newerFailingHistoryDir, { recursive: true });
  276. const passingHistoryFile = path.join(realHistoryDir, 'historical-dataset-audit.json');
  277. const smokeHistoryFile = path.join(smokeHistoryDir, 'historical-dataset-audit.json');
  278. const failingHistoryFile = path.join(newerFailingHistoryDir, 'historical-dataset-audit.json');
  279. fs.writeFileSync(passingHistoryFile, JSON.stringify(passingHistoryAudit(), null, 2), 'utf8');
  280. fs.writeFileSync(smokeHistoryFile, JSON.stringify(passingHistoryAudit(), null, 2), 'utf8');
  281. fs.writeFileSync(failingHistoryFile, JSON.stringify({ briefCount: 1, acceptance: { readyForCustomerEffectProof: false }, items: [] }, null, 2), 'utf8');
  282. fs.utimesSync(passingHistoryFile, new Date(Date.now() - 20000), new Date(Date.now() - 20000));
  283. fs.utimesSync(smokeHistoryFile, new Date(), new Date());
  284. fs.utimesSync(failingHistoryFile, new Date(Date.now() - 10000), new Date(Date.now() - 10000));
  285. const discoveredHistory = bestHistoryAudit(proofTmp);
  286. assert(path.resolve(discoveredHistory) === path.resolve(passingHistoryFile), 'history discovery should recurse and prefer a claimable non-smoke audit');
  287. const currentCustomerDir = path.join(proofTmp, 'latest-real-run', 'customer-effect-audit');
  288. const staleCustomerDir = path.join(proofTmp, 'other-real-run', 'customer-effect-audit');
  289. fs.mkdirSync(currentCustomerDir, { recursive: true });
  290. fs.mkdirSync(staleCustomerDir, { recursive: true });
  291. const currentCustomerFile = path.join(currentCustomerDir, 'customer-effect-summary.json');
  292. const staleCustomerFile = path.join(staleCustomerDir, 'customer-effect-summary.json');
  293. fs.writeFileSync(currentCustomerFile, JSON.stringify({
  294. ...passingCustomerEffect(failingHistoryFile),
  295. acceptance: { overallPass: false, historyReadyForCustomerEffectProof: false },
  296. customerSelectedRate: 0
  297. }, null, 2), 'utf8');
  298. fs.writeFileSync(staleCustomerFile, JSON.stringify(passingCustomerEffect(path.join('outputs', 'stale-history-audit', 'historical-dataset-audit.json')), null, 2), 'utf8');
  299. fs.utimesSync(currentCustomerFile, new Date(Date.now() - 10000), new Date(Date.now() - 10000));
  300. fs.utimesSync(staleCustomerFile, new Date(), new Date());
  301. const discoveredCustomerEffect = bestCustomerEffectSummary(proofTmp, failingHistoryFile, process.cwd());
  302. assert(path.resolve(discoveredCustomerEffect) === path.resolve(currentCustomerFile), 'customer-effect discovery should prefer summaries bound to the selected history audit');
  303. } finally {
  304. fs.rmSync(proofTmp, { recursive: true, force: true });
  305. }
  306. console.log(JSON.stringify({ ok: true, openCount: open.openCount, closedCount: closed.closedCount }, null, 2));
  307. }
  308. function assert(condition, message) {
  309. if (!condition) throw new Error(message);
  310. }
  311. function passingVideoAb() {
  312. return {
  313. acceptance: { passed: true },
  314. proofContext: {
  315. mode: 'live',
  316. collectionMode: 'live',
  317. generatedBy: 'acceptance:video-ab',
  318. requiresRuntimeCredential: true,
  319. requiresVocSocialProvider: true,
  320. requiresVideoAnalysisProvider: true
  321. },
  322. enhanced: { provider: { evidence: { providerStatus: 'ok' } } },
  323. delta: { strong: 1, evidenceCards: 3 }
  324. };
  325. }
  326. function passingVideoReadiness() {
  327. return {
  328. acceptance: { readyForVideoAbPreflight: true },
  329. counts: { realReferenceRows: 1, realCandidateRows: 1, videoUrlRows: 2 },
  330. failureCount: 0
  331. };
  332. }
  333. function failedVideoAbPreflight() {
  334. return {
  335. materialType: 'video_ab_runtime_preflight',
  336. directCustomerProof: false,
  337. proofLevel: 'not_business_proof',
  338. canCloseProofGap: false,
  339. readyForVideoAb: false,
  340. failureCount: 4,
  341. proofContext: {
  342. mode: 'not_run',
  343. collectionMode: 'not_run',
  344. generatedBy: 'acceptance:video-ab-preflight',
  345. requiresRuntimeCredential: true,
  346. requiresVocSocialProvider: true,
  347. requiresVideoAnalysisProvider: true
  348. },
  349. runtime: {
  350. runtimeCredentialPresent: false,
  351. companyResolved: false,
  352. vocSocialProviderTokenPresent: false,
  353. videoAnalysisTokenPresent: false
  354. },
  355. missingProofRequirements: [
  356. 'proofContext.requiresRuntimeCredential=true',
  357. 'company resolved for live run',
  358. 'proofContext.requiresVocSocialProvider=true',
  359. 'proofContext.requiresVideoAnalysisProvider=true'
  360. ]
  361. };
  362. }
  363. function passingCustomerEffect(historyAuditPath) {
  364. return {
  365. reviewPath: path.join('outputs', 'review-metrics-latest', 'reviewed.csv'),
  366. historyAuditPath,
  367. customerSelectedRate: 0.5,
  368. referenceCustomerSelectedRate: 0.5,
  369. manualSupplementBaseline: { available: true, source: 'history-audit', total: 10, byBrief: [{ briefId: 'brief-a', count: 10 }] },
  370. currentManualSupplement: { available: true, source: 'cli-arg', total: 5, byBrief: [] },
  371. manualSupplementReductionRate: 0.5,
  372. acceptance: {
  373. overallPass: true,
  374. reviewMetricsOverallUsable: true,
  375. customerSelectedRateMeasured: true,
  376. customerSelectedRate30Pass: true,
  377. referenceCustomerSelectedRateMeasured: true,
  378. referenceCustomerSelectedRate40Pass: true,
  379. historyReadyForCustomerEffectProof: true,
  380. manualSupplementBaselineAvailable: true,
  381. currentManualSupplementAvailable: true,
  382. manualSupplementReduction50Pass: true
  383. },
  384. missingEvidence: []
  385. };
  386. }
  387. function passingHistoryAudit() {
  388. return {
  389. generatedAt: '2026-06-11T02:48:48.000Z',
  390. minBriefs: 5,
  391. briefCount: 5,
  392. categoryCount: 3,
  393. categories: ['skincare', 'mom-baby', 'home'],
  394. withReference: 5,
  395. withManualFinalList: 5,
  396. withCustomerDecision: 5,
  397. withRejectionReason: 2,
  398. withManualSupplementBaseline: 5,
  399. missingCriticalCount: 0,
  400. acceptance: {
  401. parseOk: true,
  402. minBriefsMet: true,
  403. allHaveBriefText: true,
  404. allHaveManualFinalList: true,
  405. allHaveCustomerDecision: true,
  406. allHaveFeedbackReason: true,
  407. allHaveManualSupplementBaseline: true,
  408. categoryCoverageMet: true,
  409. referenceCoverageMeasured: true,
  410. readyForLongRun: true,
  411. readyForCustomerEffectProof: true
  412. },
  413. items: Array.from({ length: 5 }, (_, index) => ({
  414. id: `brief-${index + 1}`,
  415. parseOk: true,
  416. hasBriefText: true,
  417. hasManualFinalList: true,
  418. hasCustomerDecision: true,
  419. hasRejectionReason: index < 2,
  420. hasManualSupplementBaseline: true,
  421. manualSupplementBaseline: 2,
  422. missingCritical: []
  423. }))
  424. };
  425. }
  426. main();