task-handoff-index-smoke.js 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const ROOT = path.resolve(__dirname, '..');
  7. const DOC = path.join(ROOT, 'docs', 'tihao-handoff-index.md');
  8. const REQUIRED_PATHS = [
  9. 'docs/tihao-experience-optimization-plan.md',
  10. 'docs/business-proof-action-order.zh-CN.md',
  11. 'outputs/latest-form-index-latest/latest-form-index.md',
  12. 'outputs/real-proof-intake-bundle-latest/real-proof-intake-bundle.md',
  13. 'outputs/real-proof-closure-work-order-latest/real-proof-closure-work-order.md',
  14. 'outputs/local-seed-material-index-latest/local-seed-material-index.md',
  15. 'outputs/local-seed-to-intake-worklist-latest/local-seed-to-intake-worklist.md',
  16. 'outputs/experience-transcript-index-latest/experience-transcript-index.md',
  17. 'outputs/real-gap-material-audit-latest/real-gap-material-audit.md',
  18. 'outputs/proof-gap-request-latest/proof-gap-request-report.md',
  19. 'outputs/proof-gap-operator-pack-latest/proof-gap-operator-pack.md',
  20. 'outputs/business-execution-index-latest/business-execution-index.md',
  21. 'outputs/business-proof-progress-latest/business-proof-progress-report.md',
  22. 'outputs/plan-execution-status-latest/plan-execution-status.md',
  23. 'outputs/optimization-completion-latest/optimization-completion-report.md',
  24. 'outputs/long-run-readiness-latest/long-run-readiness-report.md',
  25. 'outputs/optimization-handoff-latest/optimization-handoff-report.md'
  26. ];
  27. const REQUIRED_PHRASES = [
  28. 'proof-gap-closure.openCount=3',
  29. '@vocmarket/tihao-sop@1.0.2',
  30. 'dist-tags.latest=1.0.2',
  31. 'real-gap-material-audit',
  32. 'realGapMaterialAudit',
  33. 'materialFoundCount=3',
  34. 'optimization-handoff.complete=false',
  35. 'optimization-status.counts.passed=31',
  36. 'optimization-handoff.externalBlockers=1',
  37. 'pipeline-scoped customer-effect',
  38. 'pipelineBlocked',
  39. 'proofGapClosure.complete=true/openCount=0',
  40. 'live-provider-overnight-proof` 不再出现在 `externalBlockers`',
  41. 'readyNotProven=video-ab-proof,historical-dataset',
  42. 'externalBlockers=customer-effect-proof',
  43. 'optimization-completion.readyForClaim=false',
  44. 'longrun:readiness.ready=false',
  45. '商务执行总表已把',
  46. 'businessExecutionIndex.longRunReadiness',
  47. '长跑前置门禁报告',
  48. 'status=blocked',
  49. 'actionCount=',
  50. '不能宣称提号率提升',
  51. '不能宣称客户效果达标',
  52. '不能宣称人工补号量下降',
  53. '不是业务证明',
  54. 'gapIds=historical-dataset',
  55. 'fillFileCount=3',
  56. 'recheckCommandCount=9',
  57. 'ownerGroupCount=2',
  58. 'ownerArtifactCount=2',
  59. 'evidenceRowCount=5',
  60. 'nextActionCount=5',
  61. 'operatorPackEvidenceRowCount=5',
  62. 'operatorPackNextActionCount=5',
  63. 'operatorPackOwnerArtifactCount=2',
  64. 'real-proof-closure-work-order',
  65. 'workOrderCount=13',
  66. 'realProofClosureWorkOrder.ownerArtifactCount=2',
  67. 'realProofClosureWorkOrder.canCloseProofGap=false',
  68. '商务执行总表已透传',
  69. 'operatorPackOwnerArtifactCount=2',
  70. '业务证明进度表已透传',
  71. 'businessProofProgress.operatorPackOwnerArtifactCount=2',
  72. 'businessProofNextActions.actionCount=3',
  73. '本地种子材料索引',
  74. 'local-seed-material-index',
  75. '本地种子转 intake 补表 worklist',
  76. 'local-seed-to-intake-worklist',
  77. 'historyDraftRows=1',
  78. 'candidateSeedRows=12',
  79. 'videoCandidateNeedsResourceRows=3',
  80. '经验逐字稿索引',
  81. 'experience-transcript-index',
  82. 'transcriptCount=2',
  83. 'coveredRuleCount=8',
  84. 'canCloseProofGap=false',
  85. '长期计划执行状态面板已透传',
  86. 'planExecutionStatus.operatorPackOwnerArtifactCount=2',
  87. 'businessExecutionOperatorPackOwnerArtifactRowCount=2',
  88. '操作包负责人附件',
  89. '真实历史 Brief 数据集',
  90. '真实候选视频资源',
  91. '真实视频 A/B live proof',
  92. 'proofContext.generatedBy=acceptance:video-ab',
  93. 'proofContext.mode=unknown',
  94. 'TIHAO_SESSION_TOKEN',
  95. '真实 live/provider 长跑证明',
  96. '商务复核和客户效果证明',
  97. '## 最近一次复验结果',
  98. 'overallReady=false',
  99. 'failureCount=6',
  100. 'readyForVideoAbPreflight=true',
  101. 'realCandidateRows=1',
  102. 'closedCount=2',
  103. 'readyForClaim=false',
  104. 'videoAbPreflightReadyForVideoAb=false',
  105. 'videoAbPreflightFailureCount=4',
  106. 'optimizationCompletion.videoAbPreflightFailureCount',
  107. 'video A/B preflight 失败上下文进入 closure/completion',
  108. 'proofGapOpenCount=3',
  109. 'failCount=',
  110. '不能用 `0` 或示例值代替',
  111. '## 接手角色分工',
  112. '## 完成判定矩阵',
  113. '## 可说与不可说',
  114. 'readyForCustomerEffectProof=true',
  115. 'readyForVideoAbPreflight=true',
  116. 'longrun:readiness',
  117. 'ready=true',
  118. 'liveEnabled=true',
  119. 'overallPass=true',
  120. 'openCount=0',
  121. 'complete=true',
  122. 'readyForClaim=true',
  123. '29 步全部 `pass`',
  124. 'round-deposition.passCount=142',
  125. 'provider mock、fallback、dry-run',
  126. 'npm run longrun:readiness -- --mode full-matrix',
  127. 'npm run optimization:completion -- --output outputs\\optimization-completion-latest',
  128. 'npm run round:refresh -- --output-root outputs --strict'
  129. ];
  130. function main() {
  131. assert(fs.existsSync(DOC), 'task handoff index doc should exist');
  132. const text = fs.readFileSync(DOC, 'utf8').replace(/^\uFEFF/, '');
  133. for (const requiredPath of REQUIRED_PATHS) {
  134. assert(text.includes(requiredPath), `task handoff index should link ${requiredPath}`);
  135. }
  136. for (const phrase of REQUIRED_PHRASES) {
  137. assert(text.includes(phrase), `task handoff index should include ${phrase}`);
  138. }
  139. assert(!/sk-[A-Za-z0-9_-]{10,}/.test(text), 'task handoff index should not contain model token patterns');
  140. assert(!/r:[A-Za-z0-9]{20,}/.test(text), 'task handoff index should not contain Parse sessionToken patterns');
  141. assert(!/Authorization\s*[:=]\s*Bearer/i.test(text), 'task handoff index should not expose authorization bearer values');
  142. const closure = readJson(path.join(ROOT, 'outputs', 'proof-gap-closure-latest', 'proof-gap-closure-summary.json'));
  143. const proofGapOperatorPack = readJson(path.join(ROOT, 'outputs', 'proof-gap-operator-pack-latest', 'proof-gap-operator-pack-summary.json'));
  144. const realProofIntakeBundle = readJson(path.join(ROOT, 'outputs', 'real-proof-intake-bundle-latest', 'real-proof-intake-bundle-summary.json'));
  145. const realProofClosureWorkOrder = readJson(path.join(ROOT, 'outputs', 'real-proof-closure-work-order-latest', 'real-proof-closure-work-order-summary.json'));
  146. const completion = readJson(path.join(ROOT, 'outputs', 'optimization-completion-latest', 'optimization-completion-summary.json'));
  147. const longRunReadiness = readJson(path.join(ROOT, 'outputs', 'long-run-readiness-latest', 'long-run-readiness-summary.json'));
  148. const latestForm = readJson(path.join(ROOT, 'outputs', 'latest-form-index-latest', 'latest-form-index-summary.json'));
  149. const handoff = readJson(path.join(ROOT, 'outputs', 'optimization-handoff-latest', 'optimization-handoff-summary.json'));
  150. const businessExecutionIndex = readJson(path.join(ROOT, 'outputs', 'business-execution-index-latest', 'business-execution-index-summary.json'));
  151. const businessProofProgress = readJson(path.join(ROOT, 'outputs', 'business-proof-progress-latest', 'business-proof-progress-summary.json'));
  152. const businessProofNextActions = readJson(path.join(ROOT, 'outputs', 'business-proof-next-actions-latest', 'business-proof-next-actions-summary.json'));
  153. const planExecutionStatus = readJson(path.join(ROOT, 'outputs', 'plan-execution-status-latest', 'plan-execution-status-summary.json'));
  154. const localSeed = readJson(path.join(ROOT, 'outputs', 'local-seed-material-index-latest', 'local-seed-material-index-summary.json'));
  155. const seedWorklist = readJson(path.join(ROOT, 'outputs', 'local-seed-to-intake-worklist-latest', 'local-seed-to-intake-worklist-summary.json'));
  156. const experienceTranscript = readJson(path.join(ROOT, 'outputs', 'experience-transcript-index-latest', 'experience-transcript-index-summary.json'));
  157. assert(closure?.complete === false && Number(closure.openCount || 0) === 3, 'current proof gap closure should still have three open gaps');
  158. assertProofGapOperatorPack(proofGapOperatorPack, 'current proof-gap operator pack');
  159. assertRealProofIntakeBundle(realProofIntakeBundle, 'current real proof intake bundle');
  160. assertRealProofClosureWorkOrder(realProofClosureWorkOrder, 'current real proof closure work order');
  161. assert(completion?.complete === false && completion?.readyForClaim === false && Number(completion.proofGapOpenCount || 0) === Number(closure.openCount || 0), 'current optimization completion audit should not be ready for claim');
  162. assert(completion?.canClaim?.tihaoRateImproved === false && completion?.canClaim?.customerEffectAchieved === false && completion?.canClaim?.manualSupplementReduced === false, 'completion audit should forbid all business-effect claims while incomplete');
  163. assertOptimizationCompletionPreflightContext(completion, 'current optimization completion');
  164. assert(longRunReadiness?.ready === false && Number(longRunReadiness.counts?.fail || 0) > 0, 'current long-run readiness should remain blocked with failing checks');
  165. assertBusinessExecutionLongRunReadiness(businessExecutionIndex, Number(longRunReadiness.counts?.fail || 0), 'current business execution index');
  166. assertBusinessExecutionOptimizationCompletionPreflight(businessExecutionIndex, completion, 'current business execution index');
  167. assertBusinessExecutionOperatorOwnerArtifacts(businessExecutionIndex, 'current business execution index');
  168. assertBusinessProofProgress(businessProofProgress, proofGapOperatorPack, businessExecutionIndex, 'current business proof progress');
  169. assertBusinessProofNextActions(businessProofNextActions, 'current business proof next actions');
  170. assert(localSeed?.proofLevel === 'not_business_proof' && localSeed?.directCustomerProof === false && localSeed?.canCloseProofGap === false, 'current local seed material index should not be business proof or close proof gaps');
  171. assert(Number(localSeed?.existingMaterialCount || 0) >= 1, 'current local seed material index should expose available seed materials');
  172. assertSeedToIntakeWorklist(seedWorklist, 'current local seed to intake worklist');
  173. assertExperienceTranscriptIndex(experienceTranscript, 'current experience transcript index');
  174. assertPlanExecutionStatus(planExecutionStatus, proofGapOperatorPack, businessExecutionIndex, 'current plan execution status');
  175. assert(latestForm?.passed === true && latestForm?.supportDocs?.some(item => item.path?.includes('tihao-handoff-index.md')), 'latest form index should expose task handoff doc');
  176. assert(latestForm?.longRunReadiness?.report?.includes('long-run-readiness-report.md') && latestForm.longRunReadiness.ready === false, 'latest form index should expose long-run readiness entry');
  177. assertOptimizationCompletionPreflightContext(latestForm?.optimizationCompletion, 'latest form index optimization completion');
  178. assertLatestFormNextActionsEntry(latestForm?.nextActions, businessProofNextActions, 'latest form index');
  179. assertPlanExecutionStatusEntry(latestForm?.planExecutionStatus, planExecutionStatus, 'latest form index');
  180. assertProofGapOperatorPackEntry(latestForm?.proofGapOperatorPack, proofGapOperatorPack, 'latest form index');
  181. assertRealProofIntakeBundleEntry(latestForm?.realProofIntakeBundle, realProofIntakeBundle, 'latest form index');
  182. assertRealProofClosureWorkOrderEntry(latestForm?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'latest form index');
  183. assert(handoff?.complete === false && handoff?.supportDocs?.some(item => item.doc?.includes('tihao-handoff-index.md')), 'handoff summary should expose task handoff doc');
  184. assertHandoffProofBlockers(handoff, 'handoff summary');
  185. assert(handoff?.longRunReadiness?.report?.includes('long-run-readiness-report.md') && handoff.longRunReadiness.ready === false, 'handoff summary should expose top-level long-run readiness entry');
  186. assert(handoff?.latestFormIndex?.longRunReadiness?.report?.includes('long-run-readiness-report.md') && handoff.latestFormIndex.longRunReadiness.ready === false, 'handoff latest form index should expose long-run readiness entry');
  187. assertOptimizationCompletionPreflightContext(handoff?.optimizationCompletion, 'handoff optimization completion');
  188. assertOptimizationCompletionPreflightContext(handoff?.latestFormIndex?.optimizationCompletion, 'handoff latest form optimization completion');
  189. assertBusinessProofProgressEntry(handoff?.businessProofProgress, businessProofProgress, 'handoff summary');
  190. assertBusinessProofNextActionsEntry(handoff?.businessProofNextActions, businessProofNextActions, 'handoff summary');
  191. assertHandoffLatestFormNextActionsEntry(handoff?.latestFormIndex, latestForm?.nextActions, 'handoff latest form index');
  192. assertPlanExecutionStatusEntry(handoff?.planExecutionStatus, planExecutionStatus, 'handoff summary');
  193. assertPlanExecutionStatusEntry(handoff?.latestFormIndex?.planExecutionStatus, planExecutionStatus, 'handoff latest form index');
  194. assertProofGapOperatorPackEntry(handoff?.proofGapOperatorPack, proofGapOperatorPack, 'handoff summary');
  195. assertProofGapOperatorPackEntry(handoff?.latestFormIndex?.proofGapOperatorPack, proofGapOperatorPack, 'handoff latest form index');
  196. assertRealProofIntakeBundleEntry(handoff?.realProofIntakeBundle, realProofIntakeBundle, 'handoff summary');
  197. assertRealProofIntakeBundleEntry(handoff?.latestFormIndex?.realProofIntakeBundle, realProofIntakeBundle, 'handoff latest form index');
  198. assertRealProofClosureWorkOrderEntry(handoff?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'handoff summary');
  199. assertRealProofClosureWorkOrderEntry(handoff?.latestFormIndex?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'handoff latest form index');
  200. assertHandoffBusinessExecutionLongRunReadiness(handoff, Number(longRunReadiness.counts?.fail || 0), 'current handoff summary');
  201. assertHandoffBusinessExecutionOptimizationCompletionPreflight(handoff, completion, 'current handoff summary');
  202. assertHandoffBusinessExecutionOperatorOwnerArtifacts(handoff, 'current handoff summary');
  203. const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-task-handoff-index-'));
  204. const latestOutput = path.join(temp, 'latest-form-index');
  205. const handoffOutput = path.join(temp, 'optimization-handoff');
  206. const businessExecutionOutput = path.join(temp, 'business-execution-index');
  207. runNode(['scripts/latest-form-index.js', '--output', latestOutput, '--strict']);
  208. runNode(['scripts/generate-optimization-handoff.js', '--output', handoffOutput]);
  209. runNode(['scripts/business-execution-index.js', '--output', businessExecutionOutput, '--strict']);
  210. const generatedLatest = readJson(path.join(latestOutput, 'latest-form-index-summary.json'));
  211. const generatedHandoff = readJson(path.join(handoffOutput, 'optimization-handoff-summary.json'));
  212. const generatedBusinessExecutionIndex = readJson(path.join(businessExecutionOutput, 'business-execution-index-summary.json'));
  213. assert(generatedLatest?.supportDocs?.some(item => item.path?.includes('tihao-handoff-index.md')), 'generated latest form index should expose task handoff doc');
  214. assert(generatedLatest?.longRunReadiness?.report?.includes('long-run-readiness-report.md'), 'generated latest form index should expose long-run readiness entry');
  215. assertOptimizationCompletionPreflightContext(generatedLatest?.optimizationCompletion, 'generated latest form optimization completion');
  216. assertLatestFormNextActionsEntry(generatedLatest?.nextActions, businessProofNextActions, 'generated latest form index');
  217. assertPlanExecutionStatusEntry(generatedLatest?.planExecutionStatus, planExecutionStatus, 'generated latest form index');
  218. assertProofGapOperatorPackEntry(generatedLatest?.proofGapOperatorPack, proofGapOperatorPack, 'generated latest form index');
  219. assertRealProofIntakeBundleEntry(generatedLatest?.realProofIntakeBundle, realProofIntakeBundle, 'generated latest form index');
  220. assertRealProofClosureWorkOrderEntry(generatedLatest?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'generated latest form index');
  221. assert(generatedHandoff?.supportDocs?.some(item => item.doc?.includes('tihao-handoff-index.md')), 'generated handoff summary should expose task handoff doc');
  222. assertHandoffProofBlockers(generatedHandoff, 'generated handoff summary');
  223. assert(generatedHandoff?.longRunReadiness?.report?.includes('long-run-readiness-report.md'), 'generated handoff should expose top-level long-run readiness entry');
  224. assertOptimizationCompletionPreflightContext(generatedHandoff?.optimizationCompletion, 'generated handoff optimization completion');
  225. assertOptimizationCompletionPreflightContext(generatedHandoff?.latestFormIndex?.optimizationCompletion, 'generated handoff latest form optimization completion');
  226. assertBusinessProofProgressEntry(generatedHandoff?.businessProofProgress, businessProofProgress, 'generated handoff summary');
  227. assertBusinessProofNextActionsEntry(generatedHandoff?.businessProofNextActions, businessProofNextActions, 'generated handoff summary');
  228. assertHandoffLatestFormNextActionsEntry(generatedHandoff?.latestFormIndex, latestForm?.nextActions, 'generated handoff latest form index');
  229. assertPlanExecutionStatusEntry(generatedHandoff?.planExecutionStatus, planExecutionStatus, 'generated handoff summary');
  230. assertProofGapOperatorPackEntry(generatedHandoff?.proofGapOperatorPack, proofGapOperatorPack, 'generated handoff summary');
  231. assertRealProofIntakeBundleEntry(generatedHandoff?.realProofIntakeBundle, realProofIntakeBundle, 'generated handoff summary');
  232. assertRealProofClosureWorkOrderEntry(generatedHandoff?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'generated handoff summary');
  233. assertRealProofClosureWorkOrderEntry(generatedHandoff?.latestFormIndex?.realProofClosureWorkOrder, realProofClosureWorkOrder, 'generated handoff latest form index');
  234. assertHandoffBusinessExecutionLongRunReadiness(generatedHandoff, Number(longRunReadiness.counts?.fail || 0), 'generated handoff summary');
  235. assertHandoffBusinessExecutionOptimizationCompletionPreflight(generatedHandoff, completion, 'generated handoff summary');
  236. assertBusinessExecutionLongRunReadiness(generatedBusinessExecutionIndex, Number(longRunReadiness.counts?.fail || 0), 'generated business execution index');
  237. assertBusinessExecutionOptimizationCompletionPreflight(generatedBusinessExecutionIndex, completion, 'generated business execution index');
  238. assertBusinessExecutionOperatorOwnerArtifacts(generatedBusinessExecutionIndex, 'generated business execution index');
  239. const readme = fs.readFileSync(path.join(ROOT, 'README.md'), 'utf8');
  240. assert(readme.includes('docs/tihao-handoff-index.md'), 'README should link task handoff index');
  241. const packageJson = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
  242. assert(packageJson.scripts['task:handoff-index:smoke'] === 'node scripts/task-handoff-index-smoke.js', 'package should expose task handoff smoke script');
  243. assert(packageJson.scripts['optimization:completion'] === 'node scripts/optimization-completion-audit.js', 'package should expose optimization completion audit script');
  244. assert(packageJson.scripts['optimization:completion:smoke'] === 'node scripts/optimization-completion-smoke.js', 'package should expose optimization completion smoke script');
  245. const acceptance = fs.readFileSync(path.join(ROOT, 'scripts', 'acceptance.js'), 'utf8');
  246. assert(acceptance.includes("task:handoff-index:smoke"), 'acceptance should run task handoff smoke');
  247. assert(acceptance.includes("optimization:completion:smoke"), 'acceptance should run optimization completion smoke');
  248. console.log(JSON.stringify({
  249. ok: true,
  250. doc: path.relative(ROOT, DOC).replace(/\\/g, '/'),
  251. openCount: closure.openCount,
  252. operatorPackRecheckCommandCount: proofGapOperatorPack.recheckCommandCount,
  253. businessExecutionOperatorPackOwnerArtifactCount: businessExecutionIndex.operatorPackOwnerArtifactCount,
  254. businessProofProgressOperatorPackOwnerArtifactCount: businessProofProgress.operatorPackOwnerArtifactCount,
  255. businessProofNextActionCount: businessProofNextActions.actions.length,
  256. handoffBusinessProofNextActionCount: handoff.businessProofNextActions.actionCount,
  257. latestFormIndexNextActionTopCount: latestForm.nextActions.topActions.length,
  258. handoffLatestFormIndexNextActionTopCount: handoff.latestFormIndex.topActions.length,
  259. planExecutionStatusOperatorPackOwnerArtifactCount: planExecutionStatus.operatorPackOwnerArtifactCount,
  260. planExecutionStatusBusinessExecutionOperatorPackOwnerArtifactRowCount: planExecutionStatus.businessExecutionOperatorPackOwnerArtifactRowCount,
  261. seedToIntakeHistoryDraftRows: seedWorklist.counts?.historyDraftRows,
  262. seedToIntakeCandidateSeedRows: seedWorklist.counts?.candidateSeedRows,
  263. seedToIntakeVideoCandidateNeedsResourceRows: seedWorklist.counts?.videoCandidateNeedsResourceRows,
  264. experienceTranscriptCount: experienceTranscript.transcriptCount,
  265. experienceTranscriptCoveredRuleCount: experienceTranscript.coveredRuleCount,
  266. realProofBundleOperatorPackEvidenceRowCount: realProofIntakeBundle.operatorPackEvidenceRowCount,
  267. realProofBundleOperatorPackNextActionCount: realProofIntakeBundle.operatorPackNextActionCount,
  268. realProofClosureWorkOrderCount: realProofClosureWorkOrder.workOrderCount,
  269. realProofClosureWorkOrderOwnerArtifactCount: realProofClosureWorkOrder.ownerArtifacts.length,
  270. readyForClaim: completion.readyForClaim,
  271. businessExecutionLongRunStatus: findBusinessExecutionLongRunRow(businessExecutionIndex)?.status,
  272. latestSupportDocCount: latestForm.supportDocs.length,
  273. handoffSupportDocCount: handoff.supportDocs.length
  274. }, null, 2));
  275. }
  276. function assertRealProofIntakeBundle(bundle, label) {
  277. assert(bundle?.passed === true, `${label} should pass`);
  278. assert(bundle?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  279. assert(bundle?.directCustomerProof === false, `${label} should preserve non-business-proof boundary`);
  280. assert(bundle?.proofLevel === 'not_business_proof', `${label} should keep not_business_proof level`);
  281. assert(Number(bundle?.proofOpenCount || 0) >= 1, `${label} should keep open proof gaps`);
  282. assert(Number(bundle?.proofGapRowCount || 0) === 5, `${label} should expose five proof gap rows`);
  283. assert(Number(bundle?.operatorPackEvidenceRowCount || 0) === 5, `${label} should expose operator evidence row count`);
  284. assert(Number(bundle?.operatorPackNextActionCount || 0) === 5, `${label} should expose operator next action count`);
  285. assert(Number(bundle?.operatorPackMissingProofRequirementCount || 0) >= 1, `${label} should expose operator missing proof requirement count`);
  286. assert(Number(bundle?.operatorPackOwnerArtifactCount || 0) >= 1, `${label} should expose operator owner artifact count`);
  287. assert(Array.isArray(bundle?.operatorPackOwnerArtifacts) && bundle.operatorPackOwnerArtifacts.length === Number(bundle.operatorPackOwnerArtifactCount || 0), `${label} should expose operator owner artifacts`);
  288. assert(bundle.operatorPackOwnerArtifacts.some(item => item.owner === '商务' && item.markdown?.includes('by-owner/business.md') && item.csv?.includes('by-owner/business.csv')), `${label} should expose business owner operator files`);
  289. if (Array.isArray(bundle?.proofGapRows)) {
  290. assert(bundle.proofGapRows.every(row => row.evidence && row.nextAction), `${label} proof gap rows should carry evidence and next action`);
  291. }
  292. }
  293. function assertHandoffProofBlockers(handoff, label) {
  294. const externalIds = new Set((handoff?.externalBlockers || []).map(item => item.id));
  295. const readyNotProvenIds = new Set((handoff?.readyNotProven || []).map(item => item.id));
  296. assert(externalIds.size === 1, `${label} should expose exactly one external proof blocker in the current state`);
  297. assert(externalIds.has('customer-effect-proof'), `${label} should keep customer-effect-proof as the only external proof blocker`);
  298. assert(!externalIds.has('live-provider-overnight-proof'), `${label} should not list closed live-provider proof as an external blocker`);
  299. assert(readyNotProvenIds.has('video-ab-proof'), `${label} should keep video A/B proof as ready-not-proven`);
  300. assert(readyNotProvenIds.has('historical-dataset'), `${label} should keep historical dataset as ready-not-proven`);
  301. }
  302. function assertRealProofIntakeBundleEntry(entry, bundle, label) {
  303. assert(entry?.markdown?.includes('real-proof-intake-bundle.md'), `${label} should link real proof intake bundle report`);
  304. assert(entry?.csv?.includes('real-proof-intake-bundle.csv'), `${label} should link real proof intake bundle csv`);
  305. assert(entry?.summary?.includes('real-proof-intake-bundle-summary.json'), `${label} should link real proof intake bundle summary`);
  306. assert(entry?.directCustomerProof === false, `${label} should preserve real proof bundle proof boundary`);
  307. assert(entry?.proofLevel === 'not_business_proof', `${label} should preserve real proof bundle proof level`);
  308. assert(Number(entry?.proofOpenCount || 0) === Number(bundle?.proofOpenCount || 0), `${label} should mirror real proof bundle open proof count`);
  309. assert(Number(entry?.proofGapRowCount || 0) === Number(bundle?.proofGapRowCount || 0), `${label} should mirror real proof bundle proof gap row count`);
  310. assert(Number(entry?.operatorPackEvidenceRowCount || 0) === Number(bundle?.operatorPackEvidenceRowCount || 0), `${label} should mirror real proof bundle operator evidence row count`);
  311. assert(Number(entry?.operatorPackNextActionCount || 0) === Number(bundle?.operatorPackNextActionCount || 0), `${label} should mirror real proof bundle operator next action count`);
  312. assert(Number(entry?.operatorPackMissingProofRequirementCount || 0) === Number(bundle?.operatorPackMissingProofRequirementCount || 0), `${label} should mirror real proof bundle operator missing proof requirement count`);
  313. assert(Number(entry?.operatorPackOwnerArtifactCount || 0) === Number(bundle?.operatorPackOwnerArtifactCount || 0), `${label} should mirror real proof bundle operator owner artifact count`);
  314. if (bundle?.operatorPackOwnerArtifacts?.some(item => item.owner === '商务/投放')) {
  315. assert(entry?.operatorPackOwnerArtifacts?.some(item => item.csv?.includes('by-owner/business-media.csv')), `${label} should mirror real proof bundle business/media owner csv when present`);
  316. }
  317. }
  318. function assertRealProofClosureWorkOrder(workOrder, label) {
  319. assert(workOrder?.passed === true, `${label} should pass`);
  320. assert(workOrder?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  321. assert(workOrder?.directCustomerProof === false, `${label} should preserve non-business-proof boundary`);
  322. assert(workOrder?.canCloseProofGap === false, `${label} should not close proof gaps`);
  323. assert(workOrder?.proofLevel === 'not_business_proof', `${label} should keep not_business_proof level`);
  324. assert(workOrder?.materialType === 'real_proof_closure_work_order', `${label} should classify material type`);
  325. assert(workOrder?.requiresHumanRealMaterial === true, `${label} should require human real material`);
  326. assert(Number(workOrder?.workOrderCount || 0) >= 5, `${label} should expose work orders`);
  327. assert(Number(workOrder?.ownerGroupCount || 0) >= 1, `${label} should expose owner groups`);
  328. assert(Array.isArray(workOrder?.ownerArtifacts) && workOrder.ownerArtifacts.length === Number(workOrder.ownerGroupCount || 0), `${label} should expose owner artifacts`);
  329. assert(workOrder.ownerArtifacts.some(item => item.markdown?.includes('by-owner/business.md') && item.csv?.includes('by-owner/business.csv')), `${label} should expose business owner files`);
  330. assert(workOrder.ownerArtifacts.some(item => item.markdown?.includes('by-owner/tech-ai.md') && item.csv?.includes('by-owner/tech-ai.csv')), `${label} should expose tech/AI owner files`);
  331. if (workOrder.ownerArtifacts.some(item => item.owner === '商务/投放')) {
  332. assert(workOrder.ownerArtifacts.some(item => item.markdown?.includes('by-owner/business-media.md') && item.csv?.includes('by-owner/business-media.csv')), `${label} should expose business/media owner files when present`);
  333. }
  334. assert(Number(workOrder?.sourceState?.proofGapOpenCount || 0) >= 1, `${label} should expose open proof gaps`);
  335. assert(Number(workOrder?.sourceState?.intakeFailureCount || 0) >= 1, `${label} should expose intake failure count`);
  336. assert(Number(workOrder?.sourceState?.realCandidateRows || 0) >= 0, `${label} should expose real candidate rows`);
  337. }
  338. function assertRealProofClosureWorkOrderEntry(entry, workOrder, label) {
  339. assert(entry?.markdown?.includes('real-proof-closure-work-order.md'), `${label} should link real proof closure work order report`);
  340. assert(entry?.csv?.includes('real-proof-closure-work-order.csv'), `${label} should link real proof closure work order csv`);
  341. assert(entry?.summary?.includes('real-proof-closure-work-order-summary.json'), `${label} should link real proof closure work order summary`);
  342. assert(entry?.directCustomerProof === false, `${label} should preserve real proof closure work order proof boundary`);
  343. assert(entry?.canCloseProofGap === false, `${label} should preserve real proof closure work order non-closure boundary`);
  344. assert(entry?.proofLevel === 'not_business_proof', `${label} should preserve real proof closure work order proof level`);
  345. assert(entry?.materialType === 'real_proof_closure_work_order', `${label} should preserve real proof closure work order material type`);
  346. assert(entry?.complete === false, `${label} should keep real proof closure work order incomplete state`);
  347. assert(Number(entry?.workOrderCount || 0) === Number(workOrder?.workOrderCount || 0), `${label} should mirror real proof closure work order count`);
  348. assert(Number(entry?.ownerGroupCount || 0) === Number(workOrder?.ownerGroupCount || 0), `${label} should mirror real proof closure owner group count`);
  349. assert(Number(entry?.ownerArtifacts?.length || 0) === Number(workOrder?.ownerArtifacts?.length || 0), `${label} should mirror real proof closure owner artifact count`);
  350. assert(Number(entry?.proofGapOpenCount || 0) === Number(workOrder?.sourceState?.proofGapOpenCount || 0), `${label} should mirror real proof closure open proof gaps`);
  351. assert(Number(entry?.intakeFailureCount || 0) === Number(workOrder?.sourceState?.intakeFailureCount || 0), `${label} should mirror real proof closure intake failures`);
  352. assert(Number(entry?.realCandidateRows || 0) === Number(workOrder?.sourceState?.realCandidateRows || 0), `${label} should mirror real proof closure real candidate rows`);
  353. if (workOrder?.ownerArtifacts?.some(item => item.owner === '商务/投放')) {
  354. assert(entry?.ownerArtifacts?.some(item => item.csv?.includes('by-owner/business-media.csv')), `${label} should mirror real proof closure business/media owner csv when present`);
  355. }
  356. assert(entry?.ownerArtifacts?.some(item => item.markdown?.includes('by-owner/tech-ai.md')), `${label} should mirror real proof closure tech owner markdown`);
  357. }
  358. function assertExperienceTranscriptIndex(index, label) {
  359. assert(index?.proofLevel === 'not_business_proof', `${label} should keep not_business_proof level`);
  360. assert(index?.materialType === 'experience_seed', `${label} should classify transcripts as experience seed`);
  361. assert(index?.directCustomerProof === false, `${label} should not be direct customer proof`);
  362. assert(index?.canCloseProofGap === false, `${label} should not close proof gaps`);
  363. assert(index?.complete === false, `${label} should not claim completion`);
  364. assert(Number(index?.transcriptCount || 0) === 4, `${label} should index four transcripts`);
  365. assert(Number(index?.coveredRuleCount || 0) === 13, `${label} should cover thirteen experience rules`);
  366. assert(Array.isArray(index?.ruleCoverage) && index.ruleCoverage.every(item => item.proofLevel === 'not_business_proof' && item.canCloseProofGap === false), `${label} rules should preserve proof boundary`);
  367. }
  368. function assertSeedToIntakeWorklist(worklist, label) {
  369. assert(worklist?.proofLevel === 'not_business_proof', `${label} should keep not_business_proof level`);
  370. assert(worklist?.materialType === 'seed_to_intake_worklist', `${label} should classify material type`);
  371. assert(worklist?.directCustomerProof === false, `${label} should not be direct customer proof`);
  372. assert(worklist?.canCloseProofGap === false, `${label} should not close proof gaps`);
  373. assert(worklist?.complete === false, `${label} should not claim completion`);
  374. assert(Number(worklist?.counts?.historyDraftRows || 0) >= 1, `${label} should expose history draft rows`);
  375. assert(Number(worklist?.counts?.candidateSeedRows || 0) >= 1, `${label} should expose candidate seed rows`);
  376. assert(Number(worklist?.counts?.videoWorklistRows || 0) >= 1, `${label} should expose video worklist rows`);
  377. assert(Number(worklist?.counts?.videoCandidateNeedsResourceRows || 0) >= 1, `${label} should expose candidate videos needing resources`);
  378. assert(Array.isArray(worklist?.targetFiles) && worklist.targetFiles.some(file => file.includes('history-data-template.csv')), `${label} should point to history intake template`);
  379. assert(Array.isArray(worklist?.targetFiles) && worklist.targetFiles.some(file => file.includes('video-resource-template.csv')), `${label} should point to video resource template`);
  380. assert(Array.isArray(worklist?.missingProofRequirements) && worklist.missingProofRequirements.length >= 1, `${label} should expose missing proof requirements`);
  381. }
  382. function assertProofGapOperatorPack(pack, label) {
  383. const gapIds = gapIdsOf(pack);
  384. for (const requiredId of ['historical-dataset', 'video-real-candidate', 'video-ab-live-proof', 'live-provider-overnight-proof', 'manual-review-and-customer-effect']) {
  385. assert(gapIds.includes(requiredId), `${label} should expose ${requiredId}`);
  386. }
  387. assert(pack?.passed === true, `${label} should pass`);
  388. assert(pack?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  389. assert(pack?.directCustomerProof === false, `${label} should preserve non-business-proof boundary`);
  390. assert(pack?.proofLevel === 'not_business_proof', `${label} should keep not_business_proof level`);
  391. assert(Number(pack?.openCount || 0) >= 1, `${label} should keep open gaps`);
  392. assert(Number(pack?.rowCount || 0) === 5, `${label} should expose five rows`);
  393. assert(Number(pack?.fillFileCount || 0) === 3, `${label} should expose fill file count`);
  394. assert(Number(pack?.recheckCommandCount || 0) === 9, `${label} should expose recheck command count`);
  395. assert(Number(pack?.ownerGroupCount || 0) === Number(pack?.ownerArtifactCount || pack?.ownerGroups?.length || 0), `${label} should expose owner group count`);
  396. assert(Number(pack?.evidenceRowCount || 0) === 5, `${label} should expose evidence row count`);
  397. assert(Number(pack?.nextActionCount || 0) === 5, `${label} should expose next action count`);
  398. assert(Number(pack?.missingProofRequirementCount || 0) >= 1, `${label} should expose missing proof requirement count`);
  399. }
  400. function assertProofGapOperatorPackEntry(entry, pack, label) {
  401. assert(entry?.markdown?.includes('proof-gap-operator-pack.md'), `${label} should link proof-gap operator pack report`);
  402. assert(entry?.csv?.includes('proof-gap-operator-pack.csv'), `${label} should link proof-gap operator pack csv`);
  403. assert(entry?.summary?.includes('proof-gap-operator-pack-summary.json'), `${label} should link proof-gap operator pack summary`);
  404. assert(entry?.directCustomerProof === false, `${label} should preserve proof-gap operator pack proof boundary`);
  405. assert(entry?.proofLevel === 'not_business_proof', `${label} should preserve proof-gap operator pack proof level`);
  406. assert(Number(entry?.openCount || 0) === Number(pack?.openCount || 0), `${label} should mirror proof-gap operator pack open count`);
  407. assert(Number(entry?.rowCount || 0) === Number(pack?.rowCount || 0), `${label} should mirror proof-gap operator pack row count`);
  408. assert(Number(entry?.fillFileCount || 0) === Number(pack?.fillFileCount || 0), `${label} should mirror proof-gap operator pack fill file count`);
  409. assert(Number(entry?.recheckCommandCount || 0) === Number(pack?.recheckCommandCount || 0), `${label} should mirror proof-gap operator pack recheck command count`);
  410. assert(Number(entry?.ownerGroupCount || 0) === Number(pack?.ownerGroupCount || 0), `${label} should mirror proof-gap operator pack owner group count`);
  411. assert(Number(entry?.evidenceRowCount || 0) === Number(pack?.evidenceRowCount || 0), `${label} should mirror proof-gap operator pack evidence row count`);
  412. assert(Number(entry?.nextActionCount || 0) === Number(pack?.nextActionCount || 0), `${label} should mirror proof-gap operator pack next action count`);
  413. for (const gapId of gapIdsOf(pack)) {
  414. assert(gapIdsOf(entry).includes(gapId), `${label} should mirror proof-gap operator pack gap ID ${gapId}`);
  415. }
  416. }
  417. function gapIdsOf(item) {
  418. if (Array.isArray(item?.gapIds)) return item.gapIds.map(value => String(value || ''));
  419. if (Array.isArray(item?.rows)) return item.rows.map(row => String(row.gapId || '')).filter(Boolean);
  420. return [];
  421. }
  422. function assertBusinessProofProgress(progress, pack, businessExecutionIndex, label) {
  423. assert(progress?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  424. assert(Number(progress?.operatorPackOwnerArtifactCount || 0) === Number(pack?.ownerArtifactCount || 0), `${label} should mirror proof-gap operator owner artifact count`);
  425. assert(Number(progress?.operatorPackOwnerArtifactCount || 0) === Number((pack?.ownerArtifacts || []).length), `${label} should expose current operator owner artifacts`);
  426. assert(Number(progress?.observed?.businessExecutionOperatorPackOwnerArtifactRowCount || 0) === Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0), `${label} should mirror business execution operator owner artifact row count`);
  427. assert(Number(progress?.counts?.blocked_by_external_data || 0) >= 1, `${label} should expose external data blockers`);
  428. assertOperatorOwnerArtifactsMatch(progress?.operatorPackOwnerArtifacts, pack?.ownerArtifacts, label);
  429. }
  430. function assertBusinessProofProgressEntry(entry, progress, label) {
  431. assert(entry?.summary?.includes('business-proof-progress-summary.json'), `${label} should link business proof progress summary`);
  432. assert(entry?.report?.includes('business-proof-progress-report.md'), `${label} should link business proof progress report`);
  433. assert(entry?.complete === false, `${label} should keep business proof progress incomplete state`);
  434. assert(Number(entry?.operatorPackOwnerArtifactCount || 0) === Number(progress?.operatorPackOwnerArtifactCount || 0), `${label} should mirror business proof progress operator owner artifact count`);
  435. assert(Number(entry?.businessExecutionOperatorPackOwnerArtifactRowCount || 0) === Number(progress?.observed?.businessExecutionOperatorPackOwnerArtifactRowCount || 0), `${label} should mirror business proof progress business execution artifact row count`);
  436. assert(Number(entry?.counts?.blocked_by_external_data || 0) === Number(progress?.counts?.blocked_by_external_data || 0), `${label} should mirror business proof progress blocker count`);
  437. assertOperatorOwnerArtifactsMatch(entry?.operatorPackOwnerArtifacts, progress?.operatorPackOwnerArtifacts, label);
  438. }
  439. function assertBusinessProofNextActions(nextActions, label) {
  440. assert(nextActions?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  441. const actions = Array.isArray(nextActions?.actions) ? nextActions.actions : [];
  442. assert(actions.length === Number(nextActions?.actionCount || actions.length), `${label} should mirror deduped next action count`);
  443. assert(Number(nextActions?.sourceState?.proofGapOpenCount || 0) >= 1, `${label} should keep open proof gaps`);
  444. if (nextActions?.sourceState?.pipelineStale === true) {
  445. assert(Number(nextActions.sourceState.pipelineNextActionCount || 0) === 0, `${label} should suppress current pipeline next actions when pipeline is stale`);
  446. assert(Number(nextActions.sourceState.pipelineSuppressedNextActionCount || 0) >= 1, `${label} should expose suppressed stale pipeline action count`);
  447. assert(actions.every(action => !String(action.source || '').includes('optimization-pipeline')), `${label} should not dispatch stale optimization-pipeline actions`);
  448. }
  449. assert(nextActions.actions.some(action => action.id === 'step-03' && String(action.command || '').includes('history-data-template.csv')), `${label} should expose historical data fill action`);
  450. assert(nextActions.actions.some(action => action.id === 'gap-video-ab-live-proof' && String(action.command || '').includes('acceptance:video-ab')), `${label} should expose video A/B live proof action`);
  451. }
  452. function assertBusinessProofNextActionsEntry(entry, nextActions, label) {
  453. assert(entry?.summary?.includes('business-proof-next-actions-summary.json'), `${label} should link business proof next actions summary`);
  454. assert(entry?.report?.includes('business-proof-next-actions-report.md'), `${label} should link business proof next actions report`);
  455. assert(entry?.csv?.includes('business-proof-next-actions.csv'), `${label} should link business proof next actions csv`);
  456. assert(entry?.complete === false, `${label} should keep next action queue incomplete state`);
  457. assert(Number(entry?.actionCount || 0) === Number(nextActions?.actions?.length || 0), `${label} should mirror next action count`);
  458. assert(Number(entry?.sourceState?.proofGapOpenCount || 0) === Number(nextActions?.sourceState?.proofGapOpenCount || 0), `${label} should mirror next action proof gap open count`);
  459. assert(Boolean(entry?.sourceState?.pipelineStale) === Boolean(nextActions?.sourceState?.pipelineStale), `${label} should mirror next action pipeline stale state`);
  460. assert(Number(entry?.sourceState?.pipelineSuppressedNextActionCount || 0) === Number(nextActions?.sourceState?.pipelineSuppressedNextActionCount || 0), `${label} should mirror suppressed stale pipeline action count`);
  461. assert(Array.isArray(entry?.topActions) && entry.topActions.length >= Math.min(5, nextActions.actions.length), `${label} should expose top next actions`);
  462. assert(entry.topActions.some(action => action.id === 'step-03' && String(action.command || '').includes('history-data-template.csv')), `${label} should expose top historical data action`);
  463. assert(entry.topActions.some(action => action.id === 'gap-video-ab-live-proof' && String(action.command || '').includes('acceptance:video-ab')), `${label} should expose top video A/B action`);
  464. }
  465. function assertLatestFormNextActionsEntry(entry, nextActions, label) {
  466. assert(entry?.complete === false, `${label} should keep latest form next action queue incomplete state`);
  467. assert(Number(entry?.actionCount || 0) === Number(nextActions?.actions?.length || 0), `${label} should mirror latest form next action count`);
  468. assert(Number(entry?.proofOpenCount || 0) === Number(nextActions?.sourceState?.proofGapOpenCount || 0), `${label} should mirror latest form proof gap open count`);
  469. assert(Number(entry?.sourceState?.proofGapOpenCount || 0) === Number(nextActions?.sourceState?.proofGapOpenCount || 0), `${label} should mirror latest form next action source proof gap open count`);
  470. assert(Boolean(entry?.sourceState?.pipelineStale) === Boolean(nextActions?.sourceState?.pipelineStale), `${label} should mirror latest form next action pipeline stale state`);
  471. assert(Number(entry?.sourceState?.pipelineSuppressedNextActionCount || 0) === Number(nextActions?.sourceState?.pipelineSuppressedNextActionCount || 0), `${label} should mirror latest form suppressed stale pipeline count`);
  472. assert(Number(entry?.ownerGroupCount || 0) === Number(nextActions?.ownerGroupCount || entry?.ownerGroups?.length || 0), `${label} should mirror latest form next action owner group count`);
  473. assert(Number(entry?.ownerArtifactCount || 0) === Number(nextActions?.ownerArtifactCount || entry?.ownerArtifacts?.length || 0), `${label} should mirror latest form next action owner artifact count`);
  474. assert(Array.isArray(entry?.topActions) && entry.topActions.length >= Math.min(5, nextActions.actions.length), `${label} should expose latest form top next actions`);
  475. assert(entry.topActions.some(action => action.id === 'step-03' && String(action.command || '').includes('history-data-template.csv')), `${label} should expose latest form top historical data action`);
  476. assert(entry.topActions.some(action => action.id === 'gap-video-ab-live-proof' && String(action.command || '').includes('acceptance:video-ab')), `${label} should expose latest form top video A/B action`);
  477. }
  478. function assertHandoffLatestFormNextActionsEntry(entry, latestNextActions, label) {
  479. assert(entry?.complete === false, `${label} should keep handoff latest form next action queue incomplete state`);
  480. assert(Number(entry?.actionCount || 0) === Number(latestNextActions?.actionCount || 0), `${label} should mirror handoff latest form next action count`);
  481. assert(Number(entry?.proofOpenCount || 0) === Number(latestNextActions?.proofOpenCount || 0), `${label} should mirror handoff latest form proof gap open count`);
  482. assert(Number(entry?.sourceState?.proofGapOpenCount || 0) === Number(latestNextActions?.sourceState?.proofGapOpenCount || 0), `${label} should mirror handoff latest form source proof gap open count`);
  483. assert(Boolean(entry?.sourceState?.pipelineStale) === Boolean(latestNextActions?.sourceState?.pipelineStale), `${label} should mirror handoff latest form pipeline stale state`);
  484. assert(Number(entry?.sourceState?.pipelineSuppressedNextActionCount || 0) === Number(latestNextActions?.sourceState?.pipelineSuppressedNextActionCount || 0), `${label} should mirror handoff latest form suppressed stale pipeline count`);
  485. assert(Number(entry?.ownerGroupCount || 0) === Number(latestNextActions?.ownerGroupCount || 0), `${label} should mirror handoff latest form owner group count`);
  486. assert(Number(entry?.ownerArtifactCount || 0) === Number(latestNextActions?.ownerArtifactCount || 0), `${label} should mirror handoff latest form owner artifact count`);
  487. assert(Array.isArray(entry?.topActions) && entry.topActions.length >= Math.min(5, latestNextActions?.topActions?.length || 0), `${label} should expose handoff latest form top next actions`);
  488. assert(entry.topActions.some(action => action.id === 'step-03' && String(action.command || '').includes('history-data-template.csv')), `${label} should expose handoff latest form top historical data action`);
  489. assert(entry.topActions.some(action => action.id === 'gap-video-ab-live-proof' && String(action.command || '').includes('acceptance:video-ab')), `${label} should expose handoff latest form top video A/B action`);
  490. }
  491. function assertPlanExecutionStatus(status, pack, businessExecutionIndex, label) {
  492. assert(status?.complete === false, `${label} should remain incomplete while proof gaps are open`);
  493. assert(status?.directCustomerProof !== true, `${label} should preserve non-business-proof boundary`);
  494. assert(Number(status?.rowCount || 0) === 8, `${label} should expose eight plan sections`);
  495. assert(Number(status?.proofOpenCount || 0) === Number(pack?.openCount || 0), `${label} should mirror open proof gaps`);
  496. assert(Number(status?.operatorPackOwnerArtifactCount || 0) === Number(pack?.ownerArtifactCount || 0), `${label} should mirror proof-gap operator owner artifact count`);
  497. assert(Number(status?.businessExecutionOperatorPackOwnerArtifactRowCount || 0) === Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0), `${label} should mirror business execution owner artifact rows`);
  498. assertOperatorOwnerArtifactsMatch(status?.operatorPackOwnerArtifacts, pack?.ownerArtifacts, label);
  499. }
  500. function assertPlanExecutionStatusEntry(entry, status, label) {
  501. assert(entry?.markdown?.includes('plan-execution-status.md'), `${label} should link plan execution status report`);
  502. assert(entry?.csv?.includes('plan-execution-status.csv'), `${label} should link plan execution status csv`);
  503. assert(entry?.summary?.includes('plan-execution-status-summary.json'), `${label} should link plan execution status summary`);
  504. assert(entry?.directCustomerProof === false, `${label} should preserve plan execution proof boundary`);
  505. assert(entry?.complete === false, `${label} should keep plan execution incomplete state`);
  506. assert(Number(entry?.rowCount || 0) === Number(status?.rowCount || 0), `${label} should mirror plan execution row count`);
  507. assert(Number(entry?.proofOpenCount || 0) === Number(status?.proofOpenCount || 0), `${label} should mirror plan execution proof open count`);
  508. assert(Number(entry?.operatorPackOwnerArtifactCount || 0) === Number(status?.operatorPackOwnerArtifactCount || 0), `${label} should mirror plan execution operator owner artifact count`);
  509. assert(Number(entry?.businessExecutionOperatorPackOwnerArtifactRowCount || 0) === Number(status?.businessExecutionOperatorPackOwnerArtifactRowCount || 0), `${label} should mirror plan execution business execution artifact rows`);
  510. assertOperatorOwnerArtifactsMatch(entry?.operatorPackOwnerArtifacts, status?.operatorPackOwnerArtifacts, label);
  511. }
  512. function assertBusinessExecutionLongRunReadiness(index, failCount, label) {
  513. const row = findBusinessExecutionLongRunRow(index);
  514. assert(index?.sourceFiles?.longRunReadiness?.includes('long-run-readiness-summary.json'), `${label} should expose long-run readiness source`);
  515. assert(row, `${label} should expose long-run readiness key file row`);
  516. assert(row.mainFile?.includes('long-run-readiness-report.md'), `${label} should link long-run readiness report`);
  517. assert(row.expectedArtifact?.includes('long-run-readiness-summary.json'), `${label} should link long-run readiness summary`);
  518. assert(row.status === 'blocked', `${label} should keep long-run readiness blocked while ready=false`);
  519. assert(Number(row.actionCount || 0) === failCount, `${label} should mirror long-run readiness fail count`);
  520. assert(String(row.boundary || '').includes('不能启动') && String(row.boundary || '').includes('不证明客户效果'), `${label} should preserve non-business-proof boundary`);
  521. }
  522. function assertBusinessExecutionOperatorOwnerArtifacts(index, label) {
  523. assert(Number(index?.operatorPackOwnerArtifactCount || 0) === Number(index?.operatorPackOwnerArtifacts?.length || 0), `${label} should expose current operator owner artifacts`);
  524. assert(Number(index?.operatorPackOwnerArtifactRowCount || 0) === Number(index?.operatorPackOwnerArtifacts?.length || 0), `${label} should expose current operator owner artifact rows`);
  525. assert(index?.sourceFiles?.proofGapOperatorPack?.includes('proof-gap-operator-pack-summary.json'), `${label} should expose proof-gap operator pack source`);
  526. assertOperatorOwnerArtifactsMatch(index?.operatorPackOwnerArtifacts, index?.operatorPackOwnerArtifacts, label);
  527. }
  528. function assertHandoffBusinessExecutionOperatorOwnerArtifacts(handoff, label) {
  529. const item = handoff?.businessExecutionIndex;
  530. assert(Number(item?.operatorPackOwnerArtifactCount || 0) === Number(item?.operatorPackOwnerArtifacts?.length || 0), `${label} should expose business execution operator owner artifact count`);
  531. assert(Number(item?.operatorPackOwnerArtifactRowCount || 0) === Number(item?.operatorPackOwnerArtifacts?.length || 0), `${label} should expose business execution operator owner artifact row count`);
  532. assertOperatorOwnerArtifactsMatch(item?.operatorPackOwnerArtifacts, item?.operatorPackOwnerArtifacts, label);
  533. }
  534. function assertOperatorOwnerArtifactsMatch(actualArtifacts, expectedArtifacts, label) {
  535. const actual = Array.isArray(actualArtifacts) ? actualArtifacts : [];
  536. const expected = Array.isArray(expectedArtifacts) ? expectedArtifacts.filter(item => item.owner) : [];
  537. assert(actual.length === expected.length, `${label} should mirror current operator owner artifact count`);
  538. for (const expectedItem of expected) {
  539. const item = actual.find(entry => entry.owner === expectedItem.owner);
  540. assert(item, `${label} should expose ${expectedItem.owner} operator artifact`);
  541. const markdown = normalizeOwnerArtifactPath(expectedItem.markdown);
  542. const csv = normalizeOwnerArtifactPath(expectedItem.csv);
  543. if (markdown) assert(normalizeOwnerArtifactPath(item.markdown).includes(markdown), `${label} should expose ${expectedItem.owner} operator markdown`);
  544. if (csv) assert(normalizeOwnerArtifactPath(item.csv).includes(csv), `${label} should expose ${expectedItem.owner} operator csv`);
  545. assert(Number(item?.actionCount || 0) === Number(expectedItem.actionCount || item?.actionCount || 0), `${label} should mirror ${expectedItem.owner} operator action count`);
  546. assert(Number(item?.evidenceRowCount || 0) === Number(expectedItem.evidenceRowCount || item?.evidenceRowCount || 0), `${label} should mirror ${expectedItem.owner} operator evidence rows`);
  547. assert(Number(item?.nextActionCount || 0) === Number(expectedItem.nextActionCount || item?.nextActionCount || 0), `${label} should mirror ${expectedItem.owner} operator next actions`);
  548. }
  549. }
  550. function normalizeOwnerArtifactPath(value) {
  551. const normalized = String(value || '').replace(/\\/g, '/');
  552. const index = normalized.lastIndexOf('by-owner/');
  553. return index >= 0 ? normalized.slice(index) : normalized;
  554. }
  555. function assertOperatorOwnerArtifact(ownerArtifacts, owner, slug, label) {
  556. const item = (Array.isArray(ownerArtifacts) ? ownerArtifacts : []).find(entry => entry.owner === owner);
  557. assert(item?.markdown?.includes(`by-owner/${slug}.md`), `${label} should expose ${owner} operator markdown`);
  558. assert(item?.csv?.includes(`by-owner/${slug}.csv`), `${label} should expose ${owner} operator csv`);
  559. assert(Number(item?.actionCount || 0) >= 1, `${label} should expose ${owner} operator action count`);
  560. assert(Number(item?.evidenceRowCount || 0) >= 1, `${label} should expose ${owner} operator evidence rows`);
  561. assert(Number(item?.nextActionCount || 0) >= 1, `${label} should expose ${owner} operator next actions`);
  562. }
  563. function findBusinessExecutionLongRunRow(index) {
  564. return (Array.isArray(index?.rows) ? index.rows : []).find(row => row.title === '长跑前置门禁报告');
  565. }
  566. function assertHandoffBusinessExecutionLongRunReadiness(handoff, failCount, label) {
  567. const item = handoff?.businessExecutionIndex?.longRunReadiness;
  568. assert(item?.report?.includes('long-run-readiness-report.md'), `${label} should expose business execution long-run readiness report row`);
  569. assert(item?.summary?.includes('long-run-readiness-summary.json'), `${label} should expose business execution long-run readiness summary`);
  570. assert(item.status === 'blocked', `${label} should keep business execution long-run readiness blocked while ready=false`);
  571. assert(item.ready === false, `${label} should expose business execution long-run ready=false`);
  572. assert(Number(item.failCount || 0) === failCount, `${label} should mirror business execution long-run fail count`);
  573. assert(item.directCustomerProof === false, `${label} should keep business execution long-run proof boundary`);
  574. }
  575. function assertOptimizationCompletionPreflightContext(item, label) {
  576. assert(item, `${label} should exist`);
  577. assert(item.readyForClaim === false, `${label} should remain not ready for claim`);
  578. assert(item.videoAbPreflightReadyForVideoAb === false, `${label} should expose video A/B preflight readiness`);
  579. assert(Number(item.videoAbPreflightFailureCount || 0) >= 1, `${label} should expose video A/B preflight failure count`);
  580. assert(item.directCustomerProof === false, `${label} should preserve non-customer-proof boundary`);
  581. }
  582. function assertBusinessExecutionOptimizationCompletionPreflight(index, completion, label) {
  583. const row = findBusinessExecutionOptimizationCompletionRow(index);
  584. assert(row?.mainFile?.includes('optimization-completion-report.md'), `${label} should expose optimization completion report row`);
  585. assert(row?.expectedArtifact?.includes('optimization-completion-summary.json'), `${label} should expose optimization completion summary artifact`);
  586. assert(row.status === 'blocked', `${label} should keep optimization completion blocked`);
  587. assert(String(row.acceptance || '').includes('readyForClaim=false'), `${label} should expose readyForClaim=false`);
  588. assert(String(row.acceptance || '').includes('videoAbPreflightReadyForVideoAb=false'), `${label} should expose video A/B preflight readiness`);
  589. assert(String(row.acceptance || '').includes(`videoAbPreflightFailureCount=${Number(completion?.videoAbPreflightFailureCount || 0)}`), `${label} should mirror video A/B preflight failure count`);
  590. assert(String(row.boundary || '').includes('不得宣称提号率提升'), `${label} should preserve forbidden-claim boundary`);
  591. }
  592. function assertHandoffBusinessExecutionOptimizationCompletionPreflight(handoff, completion, label) {
  593. const item = handoff?.businessExecutionIndex?.optimizationCompletion;
  594. assert(item?.report?.includes('optimization-completion-report.md'), `${label} should expose business execution optimization completion report row`);
  595. assert(item?.summary?.includes('optimization-completion-summary.json'), `${label} should expose business execution optimization completion summary`);
  596. assert(item.status === 'blocked', `${label} should keep business execution optimization completion blocked`);
  597. assert(item.readyForClaim === false, `${label} should expose business execution optimization readyForClaim=false`);
  598. assert(item.videoAbPreflightReadyForVideoAb === false, `${label} should expose business execution video A/B preflight readiness`);
  599. assert(Number(item.videoAbPreflightFailureCount || 0) === Number(completion?.videoAbPreflightFailureCount || 0), `${label} should mirror business execution video A/B preflight failure count`);
  600. assert(item.directCustomerProof === false, `${label} should keep business execution optimization proof boundary`);
  601. }
  602. function findBusinessExecutionOptimizationCompletionRow(index) {
  603. return (Array.isArray(index?.rows) ? index.rows : []).find(row => row.title === '优化完成度审计报告');
  604. }
  605. function runNode(args) {
  606. const result = spawnSync(process.execPath, args, {
  607. cwd: ROOT,
  608. encoding: 'utf8',
  609. shell: false
  610. });
  611. if (result.stdout) process.stdout.write(result.stdout);
  612. if (result.stderr) process.stderr.write(result.stderr);
  613. assert(result.status === 0, `${args.join(' ')} should pass`);
  614. }
  615. function readJson(file) {
  616. if (!fs.existsSync(file)) return null;
  617. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  618. }
  619. function assert(condition, message) {
  620. if (!condition) throw new Error(message);
  621. }
  622. if (require.main === module) main();