@hunter-harness/workflow-harness 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +25 -25
  2. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +215 -106
  6. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +8 -0
  7. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  10. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
  12. package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -0
  13. package/harness/bundles/general/claude-code/harness-plan/reference.md +6 -0
  14. package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
  15. package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
  16. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +4 -4
  19. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -0
  20. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -1
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -2
  22. package/harness/bundles/general/claude-code/scripts/harness_archive.py +404 -12
  23. package/harness/bundles/general/claude-code/scripts/harness_context.py +717 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_gate.py +1 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +77 -9
  26. package/harness/bundles/general/claude-code/scripts/harness_phase.py +1 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_plan_aggregate.py +249 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +43 -11
  29. package/harness/bundles/general/claude-code/scripts/harness_profile.py +272 -15
  30. package/harness/bundles/general/claude-code/scripts/harness_report_model.py +420 -0
  31. package/harness/bundles/general/claude-code/scripts/harness_review.py +16 -1
  32. package/harness/bundles/general/claude-code/scripts/harness_verification.py +114 -0
  33. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  34. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -0
  35. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
  37. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
  38. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  41. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
  43. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -0
  44. package/harness/bundles/general/codebuddy/harness-plan/reference.md +6 -0
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
  47. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  48. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  49. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +4 -4
  50. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -0
  51. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -1
  52. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -2
  53. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +404 -12
  54. package/harness/bundles/general/codebuddy/scripts/harness_context.py +717 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +77 -9
  57. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +1 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_plan_aggregate.py +249 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +43 -11
  60. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +272 -15
  61. package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +420 -0
  62. package/harness/bundles/general/codebuddy/scripts/harness_review.py +16 -1
  63. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +114 -0
  64. package/harness/bundles/general/codex/.harness-build.json +1 -1
  65. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -0
  66. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +215 -106
  68. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +8 -0
  69. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  72. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  74. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -0
  75. package/harness/bundles/general/codex/harness-plan/reference.md +6 -0
  76. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  77. package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
  78. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  79. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-test/SKILL.md +4 -4
  81. package/harness/bundles/general/codex/harness-test/checklist.md +1 -0
  82. package/harness/bundles/general/codex/harness-test/reference.md +2 -1
  83. package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -2
  84. package/harness/bundles/general/codex/scripts/harness_archive.py +404 -12
  85. package/harness/bundles/general/codex/scripts/harness_context.py +717 -0
  86. package/harness/bundles/general/codex/scripts/harness_gate.py +1 -0
  87. package/harness/bundles/general/codex/scripts/harness_ledger.py +77 -9
  88. package/harness/bundles/general/codex/scripts/harness_phase.py +1 -0
  89. package/harness/bundles/general/codex/scripts/harness_plan_aggregate.py +249 -0
  90. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +43 -11
  91. package/harness/bundles/general/codex/scripts/harness_profile.py +272 -15
  92. package/harness/bundles/general/codex/scripts/harness_report_model.py +420 -0
  93. package/harness/bundles/general/codex/scripts/harness_review.py +16 -1
  94. package/harness/bundles/general/codex/scripts/harness_verification.py +114 -0
  95. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  96. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -0
  97. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +215 -106
  99. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +8 -0
  100. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  102. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  103. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  105. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -0
  106. package/harness/bundles/general/cursor/harness-plan/reference.md +6 -0
  107. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  108. package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
  109. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  110. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  111. package/harness/bundles/general/cursor/harness-test/SKILL.md +4 -4
  112. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -0
  113. package/harness/bundles/general/cursor/harness-test/reference.md +2 -1
  114. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -2
  115. package/harness/bundles/general/cursor/scripts/harness_archive.py +404 -12
  116. package/harness/bundles/general/cursor/scripts/harness_context.py +717 -0
  117. package/harness/bundles/general/cursor/scripts/harness_gate.py +1 -0
  118. package/harness/bundles/general/cursor/scripts/harness_ledger.py +77 -9
  119. package/harness/bundles/general/cursor/scripts/harness_phase.py +1 -0
  120. package/harness/bundles/general/cursor/scripts/harness_plan_aggregate.py +249 -0
  121. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +43 -11
  122. package/harness/bundles/general/cursor/scripts/harness_profile.py +272 -15
  123. package/harness/bundles/general/cursor/scripts/harness_report_model.py +420 -0
  124. package/harness/bundles/general/cursor/scripts/harness_review.py +16 -1
  125. package/harness/bundles/general/cursor/scripts/harness_verification.py +114 -0
  126. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  127. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -0
  128. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +215 -106
  131. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +8 -0
  132. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  135. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  136. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  138. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -0
  139. package/harness/bundles/java/claude-code/harness-plan/reference.md +6 -0
  140. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  141. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  142. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
  143. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  144. package/harness/bundles/java/claude-code/harness-test/SKILL.md +4 -4
  145. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -0
  146. package/harness/bundles/java/claude-code/harness-test/reference.md +2 -0
  147. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -2
  148. package/harness/bundles/java/claude-code/scripts/harness_archive.py +404 -12
  149. package/harness/bundles/java/claude-code/scripts/harness_context.py +717 -0
  150. package/harness/bundles/java/claude-code/scripts/harness_gate.py +1 -0
  151. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +77 -9
  152. package/harness/bundles/java/claude-code/scripts/harness_phase.py +1 -0
  153. package/harness/bundles/java/claude-code/scripts/harness_plan_aggregate.py +249 -0
  154. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +43 -11
  155. package/harness/bundles/java/claude-code/scripts/harness_profile.py +272 -15
  156. package/harness/bundles/java/claude-code/scripts/harness_report_model.py +420 -0
  157. package/harness/bundles/java/claude-code/scripts/harness_review.py +16 -1
  158. package/harness/bundles/java/claude-code/scripts/harness_verification.py +114 -0
  159. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  160. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -0
  161. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
  164. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
  165. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  166. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  167. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  168. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  169. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  171. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -0
  172. package/harness/bundles/java/codebuddy/harness-plan/reference.md +6 -0
  173. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  174. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  175. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
  176. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +4 -4
  178. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -0
  179. package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -0
  180. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -2
  181. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +404 -12
  182. package/harness/bundles/java/codebuddy/scripts/harness_context.py +717 -0
  183. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1 -0
  184. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +77 -9
  185. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +1 -0
  186. package/harness/bundles/java/codebuddy/scripts/harness_plan_aggregate.py +249 -0
  187. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +43 -11
  188. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +272 -15
  189. package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +420 -0
  190. package/harness/bundles/java/codebuddy/scripts/harness_review.py +16 -1
  191. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +114 -0
  192. package/harness/bundles/java/codex/.harness-build.json +1 -1
  193. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -0
  194. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +215 -106
  197. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +8 -0
  198. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  199. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  200. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  201. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  202. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  203. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  204. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -0
  205. package/harness/bundles/java/codex/harness-plan/reference.md +6 -0
  206. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  207. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  208. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
  209. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  210. package/harness/bundles/java/codex/harness-test/SKILL.md +4 -4
  211. package/harness/bundles/java/codex/harness-test/checklist.md +1 -0
  212. package/harness/bundles/java/codex/harness-test/reference.md +2 -0
  213. package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -2
  214. package/harness/bundles/java/codex/scripts/harness_archive.py +404 -12
  215. package/harness/bundles/java/codex/scripts/harness_context.py +717 -0
  216. package/harness/bundles/java/codex/scripts/harness_gate.py +1 -0
  217. package/harness/bundles/java/codex/scripts/harness_ledger.py +77 -9
  218. package/harness/bundles/java/codex/scripts/harness_phase.py +1 -0
  219. package/harness/bundles/java/codex/scripts/harness_plan_aggregate.py +249 -0
  220. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +43 -11
  221. package/harness/bundles/java/codex/scripts/harness_profile.py +272 -15
  222. package/harness/bundles/java/codex/scripts/harness_report_model.py +420 -0
  223. package/harness/bundles/java/codex/scripts/harness_review.py +16 -1
  224. package/harness/bundles/java/codex/scripts/harness_verification.py +114 -0
  225. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  226. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -0
  227. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  228. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +215 -106
  230. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +8 -0
  231. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  232. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  233. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  234. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  235. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  236. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  237. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -0
  238. package/harness/bundles/java/cursor/harness-plan/reference.md +6 -0
  239. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  240. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  241. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
  242. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  243. package/harness/bundles/java/cursor/harness-test/SKILL.md +4 -4
  244. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -0
  245. package/harness/bundles/java/cursor/harness-test/reference.md +2 -0
  246. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -2
  247. package/harness/bundles/java/cursor/scripts/harness_archive.py +404 -12
  248. package/harness/bundles/java/cursor/scripts/harness_context.py +717 -0
  249. package/harness/bundles/java/cursor/scripts/harness_gate.py +1 -0
  250. package/harness/bundles/java/cursor/scripts/harness_ledger.py +77 -9
  251. package/harness/bundles/java/cursor/scripts/harness_phase.py +1 -0
  252. package/harness/bundles/java/cursor/scripts/harness_plan_aggregate.py +249 -0
  253. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +43 -11
  254. package/harness/bundles/java/cursor/scripts/harness_profile.py +272 -15
  255. package/harness/bundles/java/cursor/scripts/harness_report_model.py +420 -0
  256. package/harness/bundles/java/cursor/scripts/harness_review.py +16 -1
  257. package/harness/bundles/java/cursor/scripts/harness_verification.py +114 -0
  258. package/harness/manifests/general/claude-code.json +45 -29
  259. package/harness/manifests/general/codebuddy.json +45 -29
  260. package/harness/manifests/general/codex.json +45 -29
  261. package/harness/manifests/general/cursor.json +45 -29
  262. package/harness/manifests/java/claude-code.json +47 -31
  263. package/harness/manifests/java/codebuddy.json +47 -31
  264. package/harness/manifests/java/codex.json +47 -31
  265. package/harness/manifests/java/cursor.json +47 -31
  266. package/hunter-workflow-family.json +4 -4
  267. package/package.json +1 -1
@@ -74,6 +74,7 @@ import harness_gate as hgate # noqa: E402
74
74
  import harness_ledger as hl # noqa: E402
75
75
  import harness_paths as hp # noqa: E402
76
76
  import harness_phase as hphase # noqa: E402
77
+ import harness_report_model as hrm # noqa: E402
77
78
  import harness_review as hr # noqa: E402
78
79
 
79
80
 
@@ -1643,6 +1644,12 @@ def build_workflow_timing(
1643
1644
  if started_dt is not None and cutoff_dt is not None
1644
1645
  else 0
1645
1646
  )
1647
+ # Segment-level integer conversion can lose sub-millisecond fractions at
1648
+ # each boundary. Attribute only that positive rounding residue to the
1649
+ # otherwise-unattributed bucket so the partition remains exact.
1650
+ rounding_residue = workflow_wall - sum(categories.values())
1651
+ if rounding_residue > 0:
1652
+ categories["unattributed"] += rounding_residue
1646
1653
  stage_wall = union_ms(active_intervals)
1647
1654
  sessions: list[dict[str, Any]] = []
1648
1655
  session_start = started_dt
@@ -2645,10 +2652,9 @@ def _typed_test_metrics(entry: dict[str, Any], *, total_key: str) -> dict[str, A
2645
2652
  "executionRate": f"{executed / total:.0%}" if total > 0 else NOT_AVAILABLE,
2646
2653
  "source": "committed" if total > 0 else "not-run",
2647
2654
  }
2648
- if "blocked" in metrics:
2649
- out["blocked"] = int(metrics.get("blocked", 0) or 0)
2650
- if "skipped" in metrics:
2651
- out["skipped"] = int(metrics.get("skipped", 0) or 0)
2655
+ for optional_count in ("blocked", "skipped", "retries"):
2656
+ if optional_count in metrics:
2657
+ out[optional_count] = int(metrics.get(optional_count, 0) or 0)
2652
2658
  applicability = entry.get("applicability")
2653
2659
  if isinstance(applicability, dict):
2654
2660
  out["applicability"] = applicability
@@ -2665,18 +2671,22 @@ def build_verification_projection(
2665
2671
  apiContract and browserE2E are distinct typed projections; the legacy
2666
2672
  apiTests mapping is retained for schema compatibility.
2667
2673
  """
2668
- validations = (ledger or {}).get("validations") or {}
2674
+ effective_ledger = _deepcopy_json(ledger or {})
2675
+ projected_validations = hrm.latest_terminal_validations(effective_ledger)
2676
+ if projected_validations:
2677
+ effective_ledger["validations"] = projected_validations
2678
+ validations = effective_ledger.get("validations") or {}
2669
2679
  projection: dict[str, Any] = {
2670
- "unitTests": _ledger_unit_tests(ledger),
2671
- "apiTests": _ledger_api_tests(ledger, change_dir=change_dir),
2672
- "dbCompatibility": _ledger_db_compat(ledger),
2680
+ "unitTests": _ledger_unit_tests(effective_ledger),
2681
+ "apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
2682
+ "dbCompatibility": _ledger_db_compat(effective_ledger),
2673
2683
  }
2674
2684
  api_contract = validations.get("apiContract")
2675
2685
  if isinstance(api_contract, dict):
2676
2686
  projection["apiContract"] = _typed_test_metrics(
2677
2687
  api_contract, total_key="scenariosTotal"
2678
2688
  )
2679
- browser_e2e = validations.get("browserE2E")
2689
+ browser_e2e = validations.get("browserTest") or validations.get("browserE2E")
2680
2690
  if isinstance(browser_e2e, dict):
2681
2691
  projection["browserE2E"] = _typed_test_metrics(browser_e2e, total_key="total")
2682
2692
  performance = validations.get("performance")
@@ -3023,8 +3033,10 @@ def _compute_final_status(
3023
3033
  verification: dict[str, Any],
3024
3034
  ) -> tuple[str, list[str]]:
3025
3035
  api = verification.get("apiTests") or {}
3036
+ browser = verification.get("browserE2E") or {}
3026
3037
  db = str(verification.get("dbCompatibility") or "")
3027
3038
  api_status = str(api.get("status") or "")
3039
+ browser_status = str(browser.get("status") or "")
3028
3040
  reasons: list[str] = []
3029
3041
  for phase, v in stage_status.items():
3030
3042
  if v == "FAIL":
@@ -3036,12 +3048,18 @@ def _compute_final_status(
3036
3048
  return "FAIL", [f"unitTests.errors={unit.get('errors')}"]
3037
3049
  if int(api.get("failed") or 0) > 0:
3038
3050
  return "FAIL", [f"apiTests.failed={api.get('failed')}"]
3051
+ if int(browser.get("failed") or 0) > 0:
3052
+ return "FAIL", [f"browserE2E.failed={browser.get('failed')}"]
3053
+ if browser_status == "FAIL":
3054
+ return "FAIL", ["browserE2E.status=FAIL"]
3039
3055
  conditional = {
3040
3056
  "USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
3041
3057
  "NOT_RUN", "PARTIAL",
3042
3058
  }
3043
3059
  if api_status in conditional:
3044
3060
  reasons.append(f"apiTests.status={api_status}")
3061
+ if browser_status in conditional:
3062
+ reasons.append(f"browserE2E.status={browser_status}")
3045
3063
  if db in conditional:
3046
3064
  reasons.append(f"dbCompatibility={db}")
3047
3065
  if reasons:
@@ -3258,6 +3276,7 @@ def _review_summary(
3258
3276
  "redConfirmed": 0,
3259
3277
  "yellowFixed": 0,
3260
3278
  "yellowDeferred": 0,
3279
+ "currentRiskCount": 0,
3261
3280
  "summary": "",
3262
3281
  }
3263
3282
  sidecar = hr.findings_path(change_dir)
@@ -3287,6 +3306,8 @@ def _review_summary(
3287
3306
  for item in yellow_items
3288
3307
  if item.get("disposition") in {"DEFERRED", "ACCEPTED_RISK"}
3289
3308
  ),
3309
+ "currentRiskCount": int(status.get("currentRiskCount") or 0),
3310
+ "currentRisks": list(status.get("currentRisks") or []),
3290
3311
  "summary": f"structured review run {status.get('runId') or 'unknown'}",
3291
3312
  }
3292
3313
  )
@@ -3759,6 +3780,7 @@ def collect_summary_data(
3759
3780
  data["archiveCommit"] = identity.get("archiveCommit") or data.get("finalCommit")
3760
3781
  data["environmentHash"] = identity.get("environmentHash") or NOT_AVAILABLE
3761
3782
  data["changeIdentity"] = identity
3783
+ data = hrm.apply_identity_mirrors(data)
3762
3784
 
3763
3785
  # diffStat / changedFiles
3764
3786
  if not for_replay or not data.get("changedFiles"):
@@ -3823,8 +3845,11 @@ def collect_summary_data(
3823
3845
  data["gitFacts"] = {
3824
3846
  "baseCommit": data.get("baseCommit") or "",
3825
3847
  "finalCommit": data.get("finalCommit") or "",
3848
+ "productCommit": data.get("productCommit") or "",
3826
3849
  "featureMergeHash": data.get("featureMergeHash") or "",
3827
3850
  "releaseTipHash": data.get("releaseTipHash") or "",
3851
+ "productTreeHash": data.get("productTreeHash") or "",
3852
+ "environmentHash": data.get("environmentHash") or "",
3828
3853
  "filesChanged": int(diff_for_facts.get("filesChanged") or 0),
3829
3854
  "insertions": int(diff_for_facts.get("insertions") or 0),
3830
3855
  "deletions": int(diff_for_facts.get("deletions") or 0),
@@ -3854,10 +3879,12 @@ def collect_summary_data(
3854
3879
  data["artifactStorage"] = (
3855
3880
  storage if isinstance(storage, dict) else {}
3856
3881
  )
3882
+ data["artifactStorage"]["available"] = True
3857
3883
  sources.append("evidence/artifact-storage.json")
3858
3884
  else:
3859
3885
  data["artifactStorage"] = {
3860
3886
  "schemaVersion": 1,
3887
+ "available": False,
3861
3888
  "artifactCount": 0,
3862
3889
  "bytesAdded": 0,
3863
3890
  "bytesReused": 0,
@@ -3885,6 +3912,8 @@ def collect_summary_data(
3885
3912
  existing if for_replay else None,
3886
3913
  projected_events,
3887
3914
  )
3915
+ review_status = hr.status(change_dir)
3916
+ data["reviewFindings"] = list(review_status.get("items") or [])
3888
3917
  if not for_replay:
3889
3918
  data["timeline"] = _timeline_from_events(event_summary, projected_events)
3890
3919
  else:
@@ -3934,6 +3963,8 @@ def collect_summary_data(
3934
3963
  data.setdefault("finalStatusReasons", [])
3935
3964
  data.setdefault("riskTier", "unknown")
3936
3965
 
3966
+ data["normalizedReport"] = hrm.normalize_report(data)
3967
+
3937
3968
  # archiveManifest
3938
3969
  am = {
3939
3970
  "movedFiles": 0,
@@ -4040,7 +4071,7 @@ def resolve_node_path(project_root: Path) -> str | None:
4040
4071
  return shutil.which("node")
4041
4072
 
4042
4073
 
4043
- def render_fallback_html(summary: dict[str, Any]) -> str:
4074
+ def _render_legacy_fallback_html(summary: dict[str, Any]) -> str:
4044
4075
  """Render escaped, deterministic HTML with every validate-required fact.
4045
4076
 
4046
4077
  Used when the Node renderer is unavailable or fails. No timestamps / random
@@ -4217,6 +4248,7 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4217
4248
  ver = summary.get("verification") or {}
4218
4249
  unit = ver.get("unitTests") or {}
4219
4250
  api = ver.get("apiTests") or {}
4251
+ browser = ver.get("browserE2E") or {}
4220
4252
  parts.append("<h3>Verification</h3>")
4221
4253
  parts.append(
4222
4254
  "<p>unitTests: run={run} failures={failures} errors={errors} "
@@ -4239,6 +4271,17 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4239
4271
  blocked=esc(api.get("blocked")),
4240
4272
  )
4241
4273
  )
4274
+ parts.append(
4275
+ "<p>browserE2E: status={status} total={total} passed={passed} "
4276
+ "failed={failed} skipped={skipped} retries={retries}</p>".format(
4277
+ status=esc(browser.get("status")),
4278
+ total=esc(browser.get("total")),
4279
+ passed=esc(browser.get("passed")),
4280
+ failed=esc(browser.get("failed")),
4281
+ skipped=esc(browser.get("skipped")),
4282
+ retries=esc(browser.get("retries")),
4283
+ )
4284
+ )
4242
4285
  parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
4243
4286
 
4244
4287
  parts.append("<h3>Changed Files</h3><ul>")
@@ -4275,6 +4318,235 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4275
4318
  return "\n".join(parts) + "\n"
4276
4319
 
4277
4320
 
4321
+ def render_fallback_html(summary: dict[str, Any]) -> str:
4322
+ """Render the same executive information architecture without Node."""
4323
+
4324
+ def esc(value: Any) -> str:
4325
+ return _html_escape("" if value is None else str(value))
4326
+
4327
+ def rec(value: Any) -> dict[str, Any]:
4328
+ return value if isinstance(value, dict) else {}
4329
+
4330
+ def seq(value: Any) -> list[Any]:
4331
+ return value if isinstance(value, list) else []
4332
+
4333
+ normalized = rec(summary.get("normalizedReport"))
4334
+ outcomes = rec(normalized.get("outcomes"))
4335
+ current = rec(outcomes.get("current")) or {
4336
+ "status": summary.get("finalStatus"),
4337
+ "reasons": seq(summary.get("finalStatusReasons")),
4338
+ "stages": rec(summary.get("stageStatus")),
4339
+ "knownRisks": seq(summary.get("knownRisks")),
4340
+ }
4341
+ release = rec(outcomes.get("release")) or {
4342
+ "intent": summary.get("archiveIntent"),
4343
+ "decision": "NOT_REQUESTED"
4344
+ if summary.get("archiveIntent") == "record-only"
4345
+ else rec(summary.get("releaseDecision")).get("code"),
4346
+ "eligible": rec(summary.get("releaseDecision")).get("releaseEligible"),
4347
+ }
4348
+ identity = rec(normalized.get("identity")) or rec(summary.get("changeIdentity"))
4349
+ verification = rec(normalized.get("verification")) or rec(
4350
+ summary.get("verification")
4351
+ )
4352
+ timing = rec(normalized.get("timing")) or rec(summary.get("timing"))
4353
+ measurements = rec(normalized.get("measurements"))
4354
+ record_only = (
4355
+ release.get("intent") == "record-only"
4356
+ or summary.get("archiveIntent") == "record-only"
4357
+ )
4358
+ product_commit = (
4359
+ identity.get("productCommit")
4360
+ or summary.get("productCommit")
4361
+ or summary.get("finalCommit")
4362
+ or "N/A"
4363
+ )
4364
+ labels = [
4365
+ ("后端", ("unitTests", "dbCompatibility")),
4366
+ ("Geo", ("geo",)),
4367
+ ("前端", ("frontend",)),
4368
+ ("浏览器", ("browserE2E",)),
4369
+ ("API", ("apiTests",)),
4370
+ ]
4371
+ status_labels = {
4372
+ "OK": "通过",
4373
+ "PASS": "通过",
4374
+ "PASSED": "通过",
4375
+ "CONDITIONAL_OK": "有条件通过",
4376
+ "WARN": "警告",
4377
+ "ADVISORY": "建议",
4378
+ "FAIL": "失败",
4379
+ "FAILED": "失败",
4380
+ "ERROR": "错误",
4381
+ "BLOCKED": "阻塞",
4382
+ "NOT_RUN": "未运行",
4383
+ "SKIPPED": "已跳过",
4384
+ "NOT_APPLICABLE": "不适用",
4385
+ "UNKNOWN": "未知",
4386
+ }
4387
+
4388
+ def status_label(value: Any) -> str:
4389
+ status = str(value or "UNKNOWN").upper()
4390
+ return status_labels.get(status, status)
4391
+
4392
+ def verification_status(value: Any) -> str:
4393
+ if isinstance(value, str):
4394
+ return value
4395
+ item = rec(value)
4396
+ if item.get("status"):
4397
+ return str(item["status"])
4398
+ if int(item.get("failures") or 0) + int(item.get("errors") or 0):
4399
+ return "FAIL"
4400
+ if int(item.get("run") or item.get("total") or 0):
4401
+ return "OK"
4402
+ return "NOT_RUN"
4403
+
4404
+ def describe(value: Any) -> str:
4405
+ if isinstance(value, str):
4406
+ return value
4407
+ item = rec(value)
4408
+ return str(
4409
+ item.get("title")
4410
+ or item.get("message")
4411
+ or item.get("summary")
4412
+ or item.get("action")
4413
+ or item.get("remediation")
4414
+ or item
4415
+ )
4416
+
4417
+ def measurement_label(value: Any) -> str:
4418
+ item = rec(value)
4419
+ if item.get("state") in {"unknown", "not_applicable"}:
4420
+ return "N/A"
4421
+ if item.get("state") == "zero":
4422
+ return "0"
4423
+ if item.get("state") == "known":
4424
+ return str(item.get("value"))
4425
+ return "N/A" if value is None or value == "" else str(value)
4426
+
4427
+ risks = (
4428
+ seq(current.get("findings"))
4429
+ or seq(current.get("knownRisks"))
4430
+ or seq(summary.get("knownRisks"))
4431
+ )
4432
+ actions = seq(summary.get("manualActions"))
4433
+ groups: list[str] = []
4434
+ passed = 0
4435
+ for label, keys in labels:
4436
+ statuses = [
4437
+ verification_status(verification[key])
4438
+ for key in keys
4439
+ if key in verification
4440
+ ]
4441
+ status = (
4442
+ "FAIL"
4443
+ if any(value in {"FAIL", "ERROR", "BLOCKED"} for value in statuses)
4444
+ else "WARN"
4445
+ if any("WARN" in value or "CONDITIONAL" in value for value in statuses)
4446
+ else "NOT_APPLICABLE"
4447
+ if statuses and all(value == "NOT_APPLICABLE" for value in statuses)
4448
+ else "OK"
4449
+ if statuses
4450
+ and all(
4451
+ value in {"OK", "PASS", "PASSED", "NOT_APPLICABLE"}
4452
+ for value in statuses
4453
+ )
4454
+ else "NOT_RUN"
4455
+ )
4456
+ if status == "OK":
4457
+ passed += 1
4458
+ details = " · ".join(
4459
+ str(rec(verification.get(key)).get("passRate") or status)
4460
+ for key in keys
4461
+ if key in verification
4462
+ ) or "未配置该组验证"
4463
+ raw_details = " · ".join(
4464
+ f"{key} · status={verification_status(verification[key])}"
4465
+ + (
4466
+ f" · failed={rec(verification[key]).get('failed')}"
4467
+ if rec(verification[key]).get("failed") is not None
4468
+ else ""
4469
+ )
4470
+ for key in keys
4471
+ if key in verification
4472
+ )
4473
+ groups.append(
4474
+ f'<article class="verify" title="{esc(raw_details)}"><strong>{esc(label)}</strong>'
4475
+ f'<span title="{esc(status)}">{esc(status_label(status))}</span><small>{esc(details)}</small></article>'
4476
+ )
4477
+
4478
+ commands = seq(rec(summary.get("reportPipeline")).get("commands"))
4479
+ command_rows = "".join(
4480
+ f"<tr><td>{esc(item.get('phase') or '-')}</td>"
4481
+ f"<td><code>{esc(item.get('command'))}</code></td>"
4482
+ f"<td>{esc('OK' if int(item.get('exit_code', item.get('exitCode', 1))) == 0 else 'FAIL')}</td></tr>"
4483
+ for item in commands
4484
+ if isinstance(item, dict)
4485
+ ) or '<tr><td colspan="3">没有命令证据</td></tr>'
4486
+ files = seq(summary.get("changedFiles"))
4487
+ file_rows = "".join(
4488
+ f"<tr><td><code>{esc(item.get('path') or item.get('file'))}</code></td>"
4489
+ f"<td>+{esc(item.get('insertions') or 0)}</td>"
4490
+ f"<td>-{esc(item.get('deletions') or 0)}</td></tr>"
4491
+ for item in files
4492
+ if isinstance(item, dict)
4493
+ ) or '<tr><td colspan="3">没有变更文件证据</td></tr>'
4494
+ risk_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in risks) or "<li>当前没有未处置风险</li>"
4495
+ action_rows = "".join(f"<li>{esc(describe(item))}</li>" for item in actions) or "<li>无需人工后续动作</li>"
4496
+ release_card = (
4497
+ ""
4498
+ if record_only
4499
+ else "<article class=\"card\"><h2>发布与候选</h2>"
4500
+ f"<p>候选证明 / 发布资格:{esc(release.get('decision') or 'NOT_EVALUATED')}</p></article>"
4501
+ )
4502
+ remote = rec(measurements.get("remoteCost"))
4503
+ storage = rec(measurements.get("artifactStorage"))
4504
+ remote_totals = rec(remote.get("totals"))
4505
+ projection = rec(summary.get("projection"))
4506
+ archive_integrity = rec(summary.get("archiveIntegrity"))
4507
+ total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
4508
+ total_files_text = "" if total_files is None else str(total_files)
4509
+
4510
+ return f"""<!doctype html>
4511
+ <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4512
+ <title>Harness 最终报告 · {esc(summary.get("changeName"))}</title>
4513
+ <style>
4514
+ :root{{--bg:#f4f6f8;--card:#fff;--text:#172033;--muted:#667085;--line:#dfe5ec}}
4515
+ @media(prefers-color-scheme:dark){{:root{{--bg:#0b1119;--card:#121a26;--text:#eef3f9;--muted:#9aa8ba;--line:#2a384a}}}}
4516
+ *{{box-sizing:border-box}}html,body{{max-width:100%;overflow-x:hidden}}body{{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 "Segoe UI","Microsoft YaHei",sans-serif}}main{{width:min(1120px,calc(100% - 32px));margin:24px auto}}.hero,.card,.metric,.verify,details{{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px}}.hero h1{{margin:4px 0}}.metrics,.groups{{display:grid;grid-template-columns:repeat(5,1fr);gap:9px;margin:10px 0}}.metric strong,.verify strong{{display:block;font-size:17px}}.verify small{{display:block;color:var(--muted)}}.risk{{display:grid;grid-template-columns:1fr 1fr;gap:10px}}.card{{margin:10px 0}}details{{margin:9px 0}}summary{{cursor:pointer;font-weight:700}}details>div{{overflow:auto}}table{{width:100%;border-collapse:collapse}}td,th{{padding:8px;border-bottom:1px solid var(--line);text-align:left}}code{{overflow-wrap:anywhere}}@media(max-width:600px){{main{{width:calc(100% - 18px);margin:9px auto}}.metrics,.groups,.risk{{grid-template-columns:1fr}}}}
4517
+ </style></head><body><main>
4518
+ <section class="hero"><small>HARNESS · 管理结论</small><h1>{esc(summary.get("changeName"))}</h1>
4519
+ <p>{esc(summary.get("businessGoal") or "未记录业务目标")}</p>
4520
+ <strong title="{esc(current.get("status"))}">{esc(status_label(current.get("status")))}</strong>
4521
+ <p id="finalStatusReasons">{esc(" · ".join(str(item) for item in seq(current.get("reasons"))))}</p>
4522
+ {"<p>归档意图:仅记录 · 未请求发布</p>" if record_only else ""}</section>
4523
+ <section class="metrics"><article class="metric"><small>产品提交</small><strong><code>{esc(str(product_commit)[:10])}</code></strong></article>
4524
+ <article class="metric"><small>验证概览</small><strong>{passed}/5 组通过</strong></article>
4525
+ <article class="metric"><small>风险与动作</small><strong>{len(risks)} / {len(actions)}</strong></article>
4526
+ <article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
4527
+ <article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
4528
+ <article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
4529
+ <article class="card"><h2>风险与动作</h2><div class="risk"><section><h3>当前风险</h3><ul>{risk_rows}</ul></section><section><h3>人工动作</h3><ul>{action_rows}</ul></section></div></article>
4530
+ {release_card}
4531
+ <details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
4532
+ <p>workflowWallClock={esc(timing.get("workflowWallClockMs") if "workflowWallClockMs" in timing else "N/A")}</p>
4533
+ <p>stageActiveExecution={esc(timing.get("stageActiveExecutionMs") if "stageActiveExecutionMs" in timing else "N/A")}</p>
4534
+ <p>stageWallClockSpan={esc(timing.get("stageWallClockSpanMs") if "stageWallClockSpanMs" in timing else "N/A")}</p>
4535
+ <p>reportCutoffAt={esc(timing.get("reportCutoffAt") or "N/A")}</p>
4536
+ <p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
4537
+ <p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
4538
+ <details><summary>技术证据 · 发布与归档治理</summary><div>
4539
+ <p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
4540
+ <p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
4541
+ <p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
4542
+ <details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
4543
+ <details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
4544
+ <details><summary>技术元数据</summary><div><p>productCommit=<code>{esc(product_commit)}</code></p>
4545
+ <p>schemaVersion={esc(summary.get("schemaVersion"))}</p></div></details>
4546
+ </main></body></html>
4547
+ """
4548
+
4549
+
4278
4550
  def render_final_summary(
4279
4551
  change_dir: Path,
4280
4552
  summary_path: Path,
@@ -4725,6 +4997,7 @@ def validate_summary(
4725
4997
  ver = summary.get("verification") or {}
4726
4998
  unit = ver.get("unitTests") or {}
4727
4999
  api = ver.get("apiTests") or {}
5000
+ browser = ver.get("browserE2E") or {}
4728
5001
  pass_rate = unit.get("passRate")
4729
5002
  if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
4730
5003
  issues.append(
@@ -4745,6 +5018,22 @@ def validate_summary(
4745
5018
  "message": f"apiTests.status '{api_status}' not visible in final-summary",
4746
5019
  }
4747
5020
  )
5021
+ browser_status = str(browser.get("status") or "")
5022
+ if (
5023
+ browser_status
5024
+ and browser_status not in {"", NOT_AVAILABLE}
5025
+ and browser_status not in html
5026
+ ):
5027
+ issues.append(
5028
+ {
5029
+ "code": "missing-verification",
5030
+ "severity": "warning",
5031
+ "message": (
5032
+ f"browserE2E.status '{browser_status}' not visible "
5033
+ "in final-summary"
5034
+ ),
5035
+ }
5036
+ )
4748
5037
 
4749
5038
  # artifacts / summary-data path hints
4750
5039
  am = summary.get("archiveManifest") or {}
@@ -4777,11 +5066,23 @@ def validate_summary(
4777
5066
 
4778
5067
  # status contradiction
4779
5068
  final_status = str(summary.get("finalStatus") or "")
4780
- has_skip = api_status == "USER_SKIPPED" or str(
5069
+ browser_conditional = {
5070
+ "USER_SKIPPED",
5071
+ "BLOCKED",
5072
+ "BLOCKED_BY_ENV",
5073
+ "NOT_RUN",
5074
+ "PARTIAL",
5075
+ }
5076
+ has_skip = api_status == "USER_SKIPPED" or browser_status in browser_conditional or str(
4781
5077
  ver.get("dbCompatibility") or ""
4782
5078
  ) == "BLOCKED_BY_DBA"
4783
5079
  has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
4784
5080
  has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
5081
+ has_fail_ver = (
5082
+ has_fail_ver
5083
+ or int(browser.get("failed") or 0) > 0
5084
+ or browser_status == "FAIL"
5085
+ )
4785
5086
  stage = summary.get("stageStatus") or {}
4786
5087
  has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
4787
5088
 
@@ -4811,13 +5112,26 @@ def validate_summary(
4811
5112
  ver = summary.get("verification") or {}
4812
5113
  unit = ver.get("unitTests") or {}
4813
5114
  api = ver.get("apiTests") or {}
5115
+ browser = ver.get("browserE2E") or {}
4814
5116
  api_status = str(api.get("status") or "")
5117
+ browser_status = str(browser.get("status") or "")
4815
5118
  final_status = str(summary.get("finalStatus") or "")
4816
- has_skip = api_status == "USER_SKIPPED" or str(
5119
+ has_skip = api_status == "USER_SKIPPED" or browser_status in {
5120
+ "USER_SKIPPED",
5121
+ "BLOCKED",
5122
+ "BLOCKED_BY_ENV",
5123
+ "NOT_RUN",
5124
+ "PARTIAL",
5125
+ } or str(
4817
5126
  ver.get("dbCompatibility") or ""
4818
5127
  ) == "BLOCKED_BY_DBA"
4819
5128
  has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
4820
5129
  has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
5130
+ has_fail_ver = (
5131
+ has_fail_ver
5132
+ or int(browser.get("failed") or 0) > 0
5133
+ or browser_status == "FAIL"
5134
+ )
4821
5135
  stage = summary.get("stageStatus") or {}
4822
5136
  has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
4823
5137
  if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
@@ -4946,6 +5260,84 @@ def validate_report_adequacy(summary: dict[str, Any]) -> dict[str, Any]:
4946
5260
  "message": f"stageStatus.{phase}={status} but event reducer says {event_status}",
4947
5261
  })
4948
5262
 
5263
+ # All compatibility mirrors must be derived from the canonical identity.
5264
+ identity_doc = summary.get("changeIdentity")
5265
+ identity_doc = identity_doc if isinstance(identity_doc, dict) else {}
5266
+ canonical = hrm.canonical_identity(summary)
5267
+ mirror_sources = {
5268
+ "productCommit": summary.get("productCommit"),
5269
+ "featureMergeHash": summary.get("featureMergeHash"),
5270
+ "releaseTipHash": summary.get("releaseTipHash"),
5271
+ "productTreeHash": summary.get("productTreeHash"),
5272
+ "environmentHash": summary.get("environmentHash"),
5273
+ }
5274
+ for field, mirror in mirror_sources.items():
5275
+ expected = str(canonical.get(field) or "")
5276
+ actual = str(mirror or "")
5277
+ if field in hrm.CONTENT_HASH_FIELDS:
5278
+ expected = expected.removeprefix("sha256:")
5279
+ actual = actual.removeprefix("sha256:")
5280
+ if identity_doc.get(field) and actual and expected != actual:
5281
+ issues.append(
5282
+ {
5283
+ "code": "IDENTITY_MIRROR_MISMATCH",
5284
+ "severity": "error",
5285
+ "message": f"{field} mirror differs from changeIdentity",
5286
+ "expected": str(canonical.get(field) or ""),
5287
+ "actual": str(mirror or ""),
5288
+ }
5289
+ )
5290
+
5291
+ # Timing categories form a disjoint, wall-clock-conserving partition.
5292
+ timing = summary.get("timing")
5293
+ timing = timing if isinstance(timing, dict) else {}
5294
+ if timing:
5295
+ wall = int(timing.get("workflowWallClockMs") or 0)
5296
+ parts = sum(
5297
+ int(timing.get(key) or 0)
5298
+ for key in (
5299
+ "attributedStageUnionMs",
5300
+ "externalWaitMs",
5301
+ "pausedMs",
5302
+ "agentOrToolUnattributedMs",
5303
+ )
5304
+ )
5305
+ declared_delta = int(timing.get("conservationDeltaMs") or 0)
5306
+ if parts != wall or declared_delta != wall - parts:
5307
+ issues.append(
5308
+ {
5309
+ "code": "TIMING_CONSERVATION_MISMATCH",
5310
+ "severity": "error",
5311
+ "message": (
5312
+ f"timing categories total {parts}ms, wall clock is "
5313
+ f"{wall}ms, declared delta is {declared_delta}ms"
5314
+ ),
5315
+ }
5316
+ )
5317
+
5318
+ candidate_gate_for_release = summary.get("candidateVerification")
5319
+ candidate_gate_for_release = (
5320
+ candidate_gate_for_release
5321
+ if isinstance(candidate_gate_for_release, dict)
5322
+ else {}
5323
+ )
5324
+ candidate_status = str(
5325
+ candidate_gate_for_release.get("status")
5326
+ or candidate_gate_for_release.get("code")
5327
+ or ""
5328
+ ).upper()
5329
+ if bool(summary.get("releaseEligible")) and (
5330
+ candidate_gate_for_release.get("ok") is False
5331
+ or candidate_status in {"FAIL", "FAILED", "ERROR", "BLOCKED"}
5332
+ ):
5333
+ issues.append(
5334
+ {
5335
+ "code": "RELEASE_ELIGIBILITY_CONTRADICTION",
5336
+ "severity": "error",
5337
+ "message": "releaseEligible=true while candidate verification is not successful",
5338
+ }
5339
+ )
5340
+
4949
5341
  # Candidate receipt and summary must describe the same immutable subject.
4950
5342
  candidate_gate = summary.get("candidateVerification")
4951
5343
  candidate_gate = candidate_gate if isinstance(candidate_gate, dict) else {}