@hunter-harness/workflow-harness 0.2.41 → 0.2.43

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 (262) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/build-profile-v3.schema.json +56 -1
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -0
  5. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +8 -0
  6. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +28 -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 +42 -16
  9. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +57 -6
  10. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  11. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -1
  16. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +16 -10
  18. package/harness/bundles/general/claude-code/harness-sync/reference.md +34 -14
  19. package/harness/bundles/general/claude-code/harness-test/SKILL.md +8 -2
  20. package/harness/bundles/general/claude-code/protocols/execution-session-protocol.md +52 -0
  21. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -1
  22. package/harness/bundles/general/claude-code/protocols/verification-graph-protocol.md +6 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +4 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +290 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_environment.py +719 -35
  26. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +742 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_profile.py +34 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +1165 -3
  29. package/harness/bundles/general/claude-code/scripts/harness_service.py +76 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +127 -39
  31. package/harness/bundles/general/claude-code/scripts/harness_verification.py +408 -8
  32. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  33. package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +56 -1
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -0
  36. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
  37. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +28 -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 +41 -15
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +57 -6
  41. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  42. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -1
  47. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +16 -10
  49. package/harness/bundles/general/codebuddy/harness-sync/reference.md +34 -14
  50. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -2
  51. package/harness/bundles/general/codebuddy/protocols/execution-session-protocol.md +52 -0
  52. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -1
  53. package/harness/bundles/general/codebuddy/protocols/verification-graph-protocol.md +6 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +4 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +290 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +719 -35
  57. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +742 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +34 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +1165 -3
  60. package/harness/bundles/general/codebuddy/scripts/harness_service.py +76 -0
  61. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +127 -39
  62. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +408 -8
  63. package/harness/bundles/general/codex/.harness-build.json +1 -1
  64. package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +56 -1
  65. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-archive/reference.md +1 -0
  67. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +8 -0
  68. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +28 -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 +41 -15
  71. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +57 -6
  72. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  73. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-run/reference.md +6 -1
  78. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-sync/SKILL.md +16 -10
  80. package/harness/bundles/general/codex/harness-sync/reference.md +34 -14
  81. package/harness/bundles/general/codex/harness-test/SKILL.md +8 -2
  82. package/harness/bundles/general/codex/protocols/execution-session-protocol.md +52 -0
  83. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -1
  84. package/harness/bundles/general/codex/protocols/verification-graph-protocol.md +6 -0
  85. package/harness/bundles/general/codex/scripts/harness_archive.py +4 -0
  86. package/harness/bundles/general/codex/scripts/harness_efficiency.py +290 -0
  87. package/harness/bundles/general/codex/scripts/harness_environment.py +719 -35
  88. package/harness/bundles/general/codex/scripts/harness_fixback.py +742 -0
  89. package/harness/bundles/general/codex/scripts/harness_profile.py +34 -0
  90. package/harness/bundles/general/codex/scripts/harness_runtime.py +1165 -3
  91. package/harness/bundles/general/codex/scripts/harness_service.py +76 -0
  92. package/harness/bundles/general/codex/scripts/harness_test_runner.py +127 -39
  93. package/harness/bundles/general/codex/scripts/harness_verification.py +408 -8
  94. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  95. package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +56 -1
  96. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -0
  98. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +8 -0
  99. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +28 -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 +41 -15
  102. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +57 -6
  103. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  104. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  106. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  107. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-run/reference.md +6 -1
  109. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  110. package/harness/bundles/general/cursor/harness-sync/SKILL.md +16 -10
  111. package/harness/bundles/general/cursor/harness-sync/reference.md +34 -14
  112. package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -2
  113. package/harness/bundles/general/cursor/protocols/execution-session-protocol.md +52 -0
  114. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -1
  115. package/harness/bundles/general/cursor/protocols/verification-graph-protocol.md +6 -0
  116. package/harness/bundles/general/cursor/scripts/harness_archive.py +4 -0
  117. package/harness/bundles/general/cursor/scripts/harness_efficiency.py +290 -0
  118. package/harness/bundles/general/cursor/scripts/harness_environment.py +719 -35
  119. package/harness/bundles/general/cursor/scripts/harness_fixback.py +742 -0
  120. package/harness/bundles/general/cursor/scripts/harness_profile.py +34 -0
  121. package/harness/bundles/general/cursor/scripts/harness_runtime.py +1165 -3
  122. package/harness/bundles/general/cursor/scripts/harness_service.py +76 -0
  123. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +127 -39
  124. package/harness/bundles/general/cursor/scripts/harness_verification.py +408 -8
  125. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  126. package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +56 -1
  127. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -0
  130. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +8 -0
  131. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +28 -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 +42 -16
  134. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +57 -6
  135. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  136. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  142. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +16 -10
  143. package/harness/bundles/java/claude-code/harness-sync/reference.md +34 -14
  144. package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -2
  145. package/harness/bundles/java/claude-code/protocols/execution-session-protocol.md +52 -0
  146. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -1
  147. package/harness/bundles/java/claude-code/protocols/verification-graph-protocol.md +6 -0
  148. package/harness/bundles/java/claude-code/scripts/harness_archive.py +4 -0
  149. package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +290 -0
  150. package/harness/bundles/java/claude-code/scripts/harness_environment.py +719 -35
  151. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +742 -0
  152. package/harness/bundles/java/claude-code/scripts/harness_profile.py +34 -0
  153. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +1165 -3
  154. package/harness/bundles/java/claude-code/scripts/harness_service.py +76 -0
  155. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +127 -39
  156. package/harness/bundles/java/claude-code/scripts/harness_verification.py +408 -8
  157. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  158. package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +56 -1
  159. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -0
  162. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
  163. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +28 -0
  164. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +41 -15
  166. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +57 -6
  167. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  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 +1 -1
  171. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  172. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +16 -10
  175. package/harness/bundles/java/codebuddy/harness-sync/reference.md +34 -14
  176. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -2
  177. package/harness/bundles/java/codebuddy/protocols/execution-session-protocol.md +52 -0
  178. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -1
  179. package/harness/bundles/java/codebuddy/protocols/verification-graph-protocol.md +6 -0
  180. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +4 -0
  181. package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +290 -0
  182. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +719 -35
  183. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +742 -0
  184. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +34 -0
  185. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +1165 -3
  186. package/harness/bundles/java/codebuddy/scripts/harness_service.py +76 -0
  187. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +127 -39
  188. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +408 -8
  189. package/harness/bundles/java/codex/.harness-build.json +1 -1
  190. package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +56 -1
  191. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  193. package/harness/bundles/java/codex/harness-archive/reference.md +1 -0
  194. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +8 -0
  195. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +28 -0
  196. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +41 -15
  198. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +57 -6
  199. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  200. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  201. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  202. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  203. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  204. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  205. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  206. package/harness/bundles/java/codex/harness-sync/SKILL.md +16 -10
  207. package/harness/bundles/java/codex/harness-sync/reference.md +34 -14
  208. package/harness/bundles/java/codex/harness-test/SKILL.md +8 -2
  209. package/harness/bundles/java/codex/protocols/execution-session-protocol.md +52 -0
  210. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -1
  211. package/harness/bundles/java/codex/protocols/verification-graph-protocol.md +6 -0
  212. package/harness/bundles/java/codex/scripts/harness_archive.py +4 -0
  213. package/harness/bundles/java/codex/scripts/harness_efficiency.py +290 -0
  214. package/harness/bundles/java/codex/scripts/harness_environment.py +719 -35
  215. package/harness/bundles/java/codex/scripts/harness_fixback.py +742 -0
  216. package/harness/bundles/java/codex/scripts/harness_profile.py +34 -0
  217. package/harness/bundles/java/codex/scripts/harness_runtime.py +1165 -3
  218. package/harness/bundles/java/codex/scripts/harness_service.py +76 -0
  219. package/harness/bundles/java/codex/scripts/harness_test_runner.py +127 -39
  220. package/harness/bundles/java/codex/scripts/harness_verification.py +408 -8
  221. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  222. package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +56 -1
  223. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -0
  226. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +8 -0
  227. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +28 -0
  228. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +41 -15
  230. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +57 -6
  231. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  232. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  233. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  234. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  235. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  236. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  237. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  238. package/harness/bundles/java/cursor/harness-sync/SKILL.md +16 -10
  239. package/harness/bundles/java/cursor/harness-sync/reference.md +34 -14
  240. package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -2
  241. package/harness/bundles/java/cursor/protocols/execution-session-protocol.md +52 -0
  242. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -1
  243. package/harness/bundles/java/cursor/protocols/verification-graph-protocol.md +6 -0
  244. package/harness/bundles/java/cursor/scripts/harness_archive.py +4 -0
  245. package/harness/bundles/java/cursor/scripts/harness_efficiency.py +290 -0
  246. package/harness/bundles/java/cursor/scripts/harness_environment.py +719 -35
  247. package/harness/bundles/java/cursor/scripts/harness_fixback.py +742 -0
  248. package/harness/bundles/java/cursor/scripts/harness_profile.py +34 -0
  249. package/harness/bundles/java/cursor/scripts/harness_runtime.py +1165 -3
  250. package/harness/bundles/java/cursor/scripts/harness_service.py +76 -0
  251. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +127 -39
  252. package/harness/bundles/java/cursor/scripts/harness_verification.py +408 -8
  253. package/harness/manifests/general/claude-code.json +46 -33
  254. package/harness/manifests/general/codebuddy.json +46 -33
  255. package/harness/manifests/general/codex.json +46 -33
  256. package/harness/manifests/general/cursor.json +46 -33
  257. package/harness/manifests/java/claude-code.json +47 -34
  258. package/harness/manifests/java/codebuddy.json +47 -34
  259. package/harness/manifests/java/codex.json +47 -34
  260. package/harness/manifests/java/cursor.json +47 -34
  261. package/hunter-workflow-family.json +12 -10
  262. package/package.json +1 -1
@@ -0,0 +1,742 @@
1
+ #!/usr/bin/env python3
2
+ """Batch related fixback issues without weakening RED/GREEN evidence."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import datetime as dt
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import re
12
+ import tempfile
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ SCRIPTS_DIR = Path(__file__).resolve().parent
18
+ if str(SCRIPTS_DIR) not in sys.path:
19
+ sys.path.insert(0, str(SCRIPTS_DIR))
20
+
21
+ from harness_paths import resolve_state_dir_for_contract
22
+
23
+
24
+ SCHEMA_VERSION = 1
25
+ _BATCH_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
26
+ RISK_VERIFICATIONS = {
27
+ "security": {"security", "unitTestFull"},
28
+ "migration": {"migration", "unitTestFull"},
29
+ "public-contract": {"apiTest", "unitTestFull"},
30
+ "database": {"dbCompatibility", "unitTestFull"},
31
+ "browser": {"browserTest"},
32
+ "performance": {"performance"},
33
+ }
34
+
35
+
36
+ def now_iso() -> str:
37
+ return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
38
+
39
+
40
+ def _state_root(change_dir: Path) -> Path:
41
+ return Path(resolve_state_dir_for_contract(change_dir)).resolve()
42
+
43
+
44
+ def _batch_root(change_dir: Path) -> Path:
45
+ return _state_root(change_dir) / "fixback" / "batches"
46
+
47
+
48
+ def _batch_path(change_dir: Path, batch_id: str) -> Path:
49
+ if not _BATCH_ID.fullmatch(batch_id):
50
+ raise ValueError(f"FIXBACK_BATCH_ID_INVALID: {batch_id}")
51
+ return _batch_root(change_dir) / f"{batch_id}.json"
52
+
53
+
54
+ def _write_json(path: Path, value: dict[str, Any]) -> None:
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ text = json.dumps(value, ensure_ascii=False, indent=2) + "\n"
57
+ fd, raw = tempfile.mkstemp(
58
+ prefix=f".{path.name}.",
59
+ suffix=".tmp",
60
+ dir=str(path.parent),
61
+ )
62
+ temporary = Path(raw)
63
+ try:
64
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
65
+ handle.write(text)
66
+ handle.flush()
67
+ os.fsync(handle.fileno())
68
+ os.replace(temporary, path)
69
+ finally:
70
+ temporary.unlink(missing_ok=True)
71
+
72
+
73
+ def _load(change_dir: Path, batch_id: str) -> dict[str, Any]:
74
+ path = _batch_path(change_dir, batch_id)
75
+ try:
76
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
77
+ except FileNotFoundError as exc:
78
+ raise ValueError(f"FIXBACK_BATCH_NOT_FOUND: {batch_id}") from exc
79
+ if not isinstance(value, dict):
80
+ raise ValueError(f"FIXBACK_BATCH_CORRUPT: {batch_id}")
81
+ return value
82
+
83
+
84
+ def _all_batches(change_dir: Path) -> list[dict[str, Any]]:
85
+ root = _batch_root(change_dir)
86
+ if not root.is_dir():
87
+ return []
88
+ batches: list[dict[str, Any]] = []
89
+ for path in sorted(root.glob("*.json")):
90
+ try:
91
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
92
+ except (OSError, json.JSONDecodeError):
93
+ continue
94
+ if isinstance(value, dict):
95
+ batches.append(value)
96
+ return batches
97
+
98
+
99
+ def _required_verifications(issues: list[dict[str, Any]]) -> list[str]:
100
+ required = {"affected", "review"}
101
+ for issue in issues:
102
+ for risk in issue.get("riskTags", []):
103
+ required.update(RISK_VERIFICATIONS.get(str(risk), set()))
104
+ return sorted(required)
105
+
106
+
107
+ def _project_root(change_dir: Path) -> Path:
108
+ resolved = change_dir.resolve()
109
+ if (
110
+ resolved.parent.name == "changes"
111
+ and resolved.parent.parent.name == ".harness"
112
+ ):
113
+ return resolved.parent.parent.parent
114
+ return resolved
115
+
116
+
117
+ def _is_within(path: Path, roots: list[Path]) -> bool:
118
+ for root in roots:
119
+ try:
120
+ path.relative_to(root)
121
+ return True
122
+ except ValueError:
123
+ continue
124
+ return False
125
+
126
+
127
+ def _evidence_ledger_path(change_dir: Path) -> Path:
128
+ return _state_root(change_dir) / "fixback" / "evidence-ledger.json"
129
+
130
+
131
+ def _load_evidence_ledger(change_dir: Path) -> dict[str, Any]:
132
+ path = _evidence_ledger_path(change_dir)
133
+ if not path.is_file():
134
+ return {"schemaVersion": 1, "evidence": {}}
135
+ try:
136
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
137
+ except (OSError, json.JSONDecodeError) as exc:
138
+ raise ValueError(f"FIXBACK_EVIDENCE_LEDGER_INVALID: {exc}") from exc
139
+ if (
140
+ not isinstance(value, dict)
141
+ or value.get("schemaVersion") != 1
142
+ or not isinstance(value.get("evidence"), dict)
143
+ ):
144
+ raise ValueError("FIXBACK_EVIDENCE_LEDGER_INVALID")
145
+ return value
146
+
147
+
148
+ def _resolve_evidence_path(change_dir: Path, raw_path: str) -> Path:
149
+ raw = Path(raw_path)
150
+ contract_root = change_dir.resolve()
151
+ state_root = _state_root(change_dir)
152
+ project_root = _project_root(change_dir)
153
+ candidates = (
154
+ [raw.resolve()]
155
+ if raw.is_absolute()
156
+ else [
157
+ (contract_root / raw).resolve(),
158
+ (state_root / raw).resolve(),
159
+ (project_root / raw).resolve(),
160
+ ]
161
+ )
162
+ path = next((candidate for candidate in candidates if candidate.is_file()), None)
163
+ if path is None:
164
+ raise ValueError(f"FIXBACK_EVIDENCE_MISSING: {raw_path}")
165
+ if not _is_within(path, [contract_root, state_root, project_root]):
166
+ raise ValueError(f"FIXBACK_EVIDENCE_OUTSIDE_PROJECT: {raw_path}")
167
+ return path
168
+
169
+
170
+ def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
171
+ """Register evidence only after validating its authoritative provenance."""
172
+ path = _resolve_evidence_path(change_dir, raw_path)
173
+ try:
174
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
175
+ except (OSError, json.JSONDecodeError) as exc:
176
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}: {exc}") from exc
177
+ if not isinstance(value, dict) or value.get("schemaVersion") != 2:
178
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
179
+ kind = str(value.get("kind") or "")
180
+ status = str(value.get("status") or "").upper()
181
+ evidence_id = str(value.get("evidenceId") or "").strip()
182
+ product_identity = str(value.get("productIdentity") or "").strip()
183
+ passed_gates = value.get("passedGates")
184
+ provenance = value.get("provenance")
185
+ if (
186
+ kind not in {"red", "green", "verification", "review"}
187
+ or status not in {"FAIL", "PASS", "OK"}
188
+ or not evidence_id
189
+ or not product_identity
190
+ or not isinstance(passed_gates, list)
191
+ or any(not isinstance(item, str) or not item.strip() for item in passed_gates)
192
+ or not isinstance(provenance, dict)
193
+ ):
194
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
195
+ state_root = _state_root(change_dir)
196
+ if kind in {"red", "green", "verification"}:
197
+ if provenance.get("type") != "managed-run-session":
198
+ raise ValueError("FIXBACK_RUN_PROVENANCE_REQUIRED")
199
+ run_path = _resolve_evidence_path(
200
+ change_dir, str(provenance.get("runReceiptPath") or "")
201
+ )
202
+ expected_run_root = (state_root / "runtime" / "run-sessions").resolve()
203
+ if not _is_within(run_path, [expected_run_root]) or run_path.name != "session.json":
204
+ raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID")
205
+ try:
206
+ run = json.loads(run_path.read_text(encoding="utf-8-sig"))
207
+ except (OSError, json.JSONDecodeError) as exc:
208
+ raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID") from exc
209
+ expected_status = "FAIL" if kind == "red" else "OK"
210
+ if (
211
+ not isinstance(run, dict)
212
+ or run.get("status") != expected_status
213
+ or run.get("testProcessStarted") is not True
214
+ or run.get("productIdentity") != product_identity
215
+ or provenance.get("sessionId") != run.get("sessionId")
216
+ or provenance.get("commandHash") != run.get("commandHash")
217
+ or provenance.get("resultDigest") != run.get("resultDigest")
218
+ or not str(run.get("commandHash") or "").startswith("sha256:")
219
+ or not str(run.get("resultDigest") or "").startswith("sha256:")
220
+ or not str(run.get("endedAt") or "").strip()
221
+ ):
222
+ raise ValueError("FIXBACK_RUN_PROVENANCE_INVALID")
223
+ else:
224
+ if provenance.get("type") != "harness-review":
225
+ raise ValueError("FIXBACK_REVIEW_PROVENANCE_REQUIRED")
226
+ review_path = _resolve_evidence_path(
227
+ change_dir, str(provenance.get("reviewReportPath") or "")
228
+ )
229
+ expected_review_path = (
230
+ state_root / "reports" / "review" / "review-findings.json"
231
+ ).resolve()
232
+ if review_path != expected_review_path:
233
+ raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID")
234
+ try:
235
+ review = json.loads(review_path.read_text(encoding="utf-8-sig"))
236
+ except (OSError, json.JSONDecodeError) as exc:
237
+ raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID") from exc
238
+ source_digest = "sha256:" + hashlib.sha256(review_path.read_bytes()).hexdigest()
239
+ findings = review.get("findings") if isinstance(review, dict) else None
240
+ unresolved = [
241
+ item
242
+ for item in findings or []
243
+ if isinstance(item, dict) and item.get("severity") in {"RED", "YELLOW"}
244
+ ]
245
+ if (
246
+ not isinstance(review, dict)
247
+ or review.get("schemaVersion") != 1
248
+ or not isinstance(findings, list)
249
+ or review.get("runId") != provenance.get("reviewRunId")
250
+ or provenance.get("engine") != "harness-review-6d"
251
+ or provenance.get("sourceDigest") != source_digest
252
+ or unresolved
253
+ ):
254
+ raise ValueError("FIXBACK_REVIEW_PROVENANCE_INVALID")
255
+ record = {
256
+ "path": str(path),
257
+ "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
258
+ "evidenceId": evidence_id,
259
+ "kind": kind,
260
+ "status": status,
261
+ "productIdentity": product_identity,
262
+ "passedGates": sorted(set(passed_gates)),
263
+ "provenance": provenance,
264
+ "registeredAt": now_iso(),
265
+ }
266
+ ledger = _load_evidence_ledger(change_dir)
267
+ existing = ledger["evidence"].get(evidence_id)
268
+ if existing is not None:
269
+ comparable = {
270
+ key: value
271
+ for key, value in record.items()
272
+ if key != "registeredAt"
273
+ }
274
+ existing_comparable = {
275
+ key: value
276
+ for key, value in existing.items()
277
+ if key != "registeredAt"
278
+ } if isinstance(existing, dict) else {}
279
+ if existing_comparable != comparable:
280
+ raise ValueError(f"FIXBACK_EVIDENCE_ID_REUSED: {evidence_id}")
281
+ return existing
282
+ ledger["evidence"][evidence_id] = record
283
+ _write_json(_evidence_ledger_path(change_dir), ledger)
284
+ return record
285
+
286
+
287
+ def _evidence_record(
288
+ change_dir: Path,
289
+ raw_path: str,
290
+ *,
291
+ kind: str,
292
+ statuses: set[str],
293
+ product_identity: str | None = None,
294
+ ) -> dict[str, Any]:
295
+ path = _resolve_evidence_path(change_dir, raw_path)
296
+ try:
297
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
298
+ except (OSError, json.JSONDecodeError) as exc:
299
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}: {exc}") from exc
300
+ if not isinstance(value, dict):
301
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
302
+ status = str(value.get("status") or "").upper()
303
+ evidence_product = str(value.get("productIdentity") or "").strip()
304
+ evidence_id = str(value.get("evidenceId") or "").strip()
305
+ if (
306
+ value.get("schemaVersion") != 2
307
+ or value.get("kind") != kind
308
+ or status not in statuses
309
+ or not evidence_product
310
+ or not evidence_id
311
+ ):
312
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
313
+ passed_gates = value.get("passedGates")
314
+ if not isinstance(passed_gates, list) or any(
315
+ not isinstance(item, str) or not item.strip()
316
+ for item in passed_gates
317
+ ):
318
+ raise ValueError(f"FIXBACK_EVIDENCE_INVALID: {raw_path}")
319
+ ledger_record = _load_evidence_ledger(change_dir)["evidence"].get(evidence_id)
320
+ current_sha = hashlib.sha256(path.read_bytes()).hexdigest()
321
+ if (
322
+ not isinstance(ledger_record, dict)
323
+ or ledger_record.get("path") != str(path)
324
+ or ledger_record.get("sha256") != current_sha
325
+ or ledger_record.get("kind") != kind
326
+ or ledger_record.get("status") != status
327
+ or ledger_record.get("productIdentity") != evidence_product
328
+ or ledger_record.get("passedGates") != sorted(set(passed_gates))
329
+ ):
330
+ raise ValueError(f"FIXBACK_EVIDENCE_UNREGISTERED: {raw_path}")
331
+ if product_identity is not None and evidence_product != product_identity:
332
+ raise ValueError(
333
+ "FIXBACK_EVIDENCE_IDENTITY_MISMATCH: "
334
+ f"{raw_path}: expected={product_identity} actual={evidence_product}"
335
+ )
336
+ return {
337
+ "path": str(path),
338
+ "sha256": current_sha,
339
+ "evidenceId": evidence_id,
340
+ "kind": kind,
341
+ "status": status,
342
+ "productIdentity": evidence_product,
343
+ "passedGates": sorted(set(passed_gates)),
344
+ "provenance": ledger_record["provenance"],
345
+ }
346
+
347
+
348
+ def _changed_file_record(change_dir: Path, raw_path: str) -> dict[str, str]:
349
+ root = _project_root(change_dir)
350
+ raw = Path(raw_path)
351
+ path = raw.resolve() if raw.is_absolute() else (root / raw).resolve()
352
+ if not path.is_file():
353
+ raise ValueError(f"FIXBACK_CHANGED_FILE_MISSING: {raw_path}")
354
+ if not _is_within(path, [root]):
355
+ raise ValueError(f"FIXBACK_CHANGED_FILE_OUTSIDE_PROJECT: {raw_path}")
356
+ return {
357
+ "path": str(path.relative_to(root)).replace("\\", "/"),
358
+ "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
359
+ }
360
+
361
+
362
+ def _evidence_digest_matches(record: Any) -> bool:
363
+ if not isinstance(record, dict):
364
+ return False
365
+ try:
366
+ path = Path(str(record["path"]))
367
+ expected = str(record["sha256"])
368
+ return path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == expected
369
+ except (KeyError, OSError):
370
+ return False
371
+
372
+
373
+ def open_batch(
374
+ change_dir: Path,
375
+ *,
376
+ batch_id: str,
377
+ product_identity: str,
378
+ root_cause: str,
379
+ ) -> dict[str, Any]:
380
+ if not product_identity.strip():
381
+ raise ValueError("FIXBACK_PRODUCT_IDENTITY_REQUIRED")
382
+ if not root_cause.strip():
383
+ raise ValueError("FIXBACK_ROOT_CAUSE_REQUIRED")
384
+ open_batches = [
385
+ item for item in _all_batches(change_dir)
386
+ if item.get("status") == "OPEN"
387
+ ]
388
+ if open_batches:
389
+ raise ValueError(
390
+ "FIXBACK_BATCH_ALREADY_OPEN: " + str(open_batches[0].get("batchId"))
391
+ )
392
+ path = _batch_path(change_dir, batch_id)
393
+ if path.exists():
394
+ raise ValueError(f"FIXBACK_BATCH_EXISTS: {batch_id}")
395
+ opened_at = now_iso()
396
+ batch = {
397
+ "schemaVersion": SCHEMA_VERSION,
398
+ "batchId": batch_id,
399
+ "status": "OPEN",
400
+ "rootCause": root_cause,
401
+ "openedAt": opened_at,
402
+ "updatedAt": opened_at,
403
+ "closedAt": None,
404
+ "baseProductIdentity": product_identity,
405
+ "finalProductIdentity": None,
406
+ "issues": [],
407
+ "changedFiles": [],
408
+ "requiredVerifications": ["affected", "review"],
409
+ "receipts": {
410
+ "affected": None,
411
+ "review": None,
412
+ },
413
+ "verificationRuns": {
414
+ "affected": 0,
415
+ "review": 0,
416
+ },
417
+ }
418
+ _write_json(path, batch)
419
+ return batch
420
+
421
+
422
+ def add_issue(
423
+ change_dir: Path,
424
+ *,
425
+ batch_id: str,
426
+ issue_id: str,
427
+ summary: str,
428
+ risk_tags: list[str],
429
+ ) -> dict[str, Any]:
430
+ batch = _load(change_dir, batch_id)
431
+ if batch.get("status") != "OPEN":
432
+ raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
433
+ if any(item.get("issueId") == issue_id for item in batch["issues"]):
434
+ raise ValueError(f"FIXBACK_ISSUE_EXISTS: {issue_id}")
435
+ issue = {
436
+ "issueId": issue_id,
437
+ "summary": summary,
438
+ "status": "OPEN",
439
+ "riskTags": sorted(set(risk_tags)),
440
+ "redEvidence": None,
441
+ "greenEvidence": None,
442
+ "changedFiles": [],
443
+ "resolvedAt": None,
444
+ }
445
+ batch["issues"].append(issue)
446
+ batch["requiredVerifications"] = _required_verifications(batch["issues"])
447
+ batch["updatedAt"] = now_iso()
448
+ _write_json(_batch_path(change_dir, batch_id), batch)
449
+ return issue
450
+
451
+
452
+ def resolve_issue(
453
+ change_dir: Path,
454
+ *,
455
+ batch_id: str,
456
+ issue_id: str,
457
+ red_evidence: str,
458
+ green_evidence: str,
459
+ changed_files: list[str],
460
+ ) -> dict[str, Any]:
461
+ if not red_evidence.strip() or not green_evidence.strip():
462
+ raise ValueError(f"FIXBACK_RED_GREEN_REQUIRED: {issue_id}")
463
+ batch = _load(change_dir, batch_id)
464
+ if batch.get("status") != "OPEN":
465
+ raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
466
+ issue = next(
467
+ (item for item in batch["issues"] if item.get("issueId") == issue_id),
468
+ None,
469
+ )
470
+ if issue is None:
471
+ raise ValueError(f"FIXBACK_ISSUE_NOT_FOUND: {issue_id}")
472
+ red_record = _evidence_record(
473
+ change_dir,
474
+ red_evidence,
475
+ kind="red",
476
+ statuses={"FAIL"},
477
+ product_identity=str(batch["baseProductIdentity"]),
478
+ )
479
+ green_record = _evidence_record(
480
+ change_dir,
481
+ green_evidence,
482
+ kind="green",
483
+ statuses={"PASS", "OK"},
484
+ )
485
+ if red_record["evidenceId"] == green_record["evidenceId"]:
486
+ raise ValueError(f"FIXBACK_EVIDENCE_ID_REUSED: {issue_id}")
487
+ changed_records = [
488
+ _changed_file_record(change_dir, raw_path)
489
+ for raw_path in sorted(set(changed_files))
490
+ ]
491
+ resolved_at = now_iso()
492
+ issue.update(
493
+ {
494
+ "status": "RESOLVED",
495
+ "redEvidence": red_record,
496
+ "greenEvidence": green_record,
497
+ "changedFiles": changed_records,
498
+ "resolvedAt": resolved_at,
499
+ }
500
+ )
501
+ batch["changedFiles"] = sorted(
502
+ {
503
+ record["path"]
504
+ for item in batch["issues"]
505
+ for record in item.get("changedFiles", [])
506
+ }
507
+ )
508
+ batch["updatedAt"] = resolved_at
509
+ _write_json(_batch_path(change_dir, batch_id), batch)
510
+ return issue
511
+
512
+
513
+ def close_batch(
514
+ change_dir: Path,
515
+ *,
516
+ batch_id: str,
517
+ final_product_identity: str,
518
+ affected_receipt: str,
519
+ review_receipt: str,
520
+ ) -> dict[str, Any]:
521
+ batch = _load(change_dir, batch_id)
522
+ if batch.get("status") != "OPEN":
523
+ raise ValueError(f"FIXBACK_BATCH_CLOSED: {batch_id}")
524
+ unresolved = [
525
+ str(item.get("issueId"))
526
+ for item in batch["issues"]
527
+ if item.get("status") != "RESOLVED"
528
+ or not item.get("redEvidence")
529
+ or not item.get("greenEvidence")
530
+ ]
531
+ if unresolved:
532
+ raise ValueError(
533
+ "FIXBACK_ISSUES_UNRESOLVED: " + ",".join(unresolved)
534
+ )
535
+ if not affected_receipt.strip() or not review_receipt.strip():
536
+ raise ValueError("FIXBACK_BATCH_RECEIPTS_REQUIRED")
537
+ if not final_product_identity.strip():
538
+ raise ValueError("FIXBACK_FINAL_IDENTITY_REQUIRED")
539
+ for issue in batch["issues"]:
540
+ green = issue.get("greenEvidence")
541
+ if not isinstance(green, dict) or (
542
+ green.get("productIdentity") != final_product_identity
543
+ ):
544
+ raise ValueError(
545
+ "FIXBACK_GREEN_IDENTITY_MISMATCH: "
546
+ + str(issue.get("issueId"))
547
+ )
548
+ affected_record = _evidence_record(
549
+ change_dir,
550
+ affected_receipt,
551
+ kind="verification",
552
+ statuses={"PASS", "OK"},
553
+ product_identity=final_product_identity,
554
+ )
555
+ review_record = _evidence_record(
556
+ change_dir,
557
+ review_receipt,
558
+ kind="review",
559
+ statuses={"PASS", "OK"},
560
+ product_identity=final_product_identity,
561
+ )
562
+ passed_gates = set(affected_record["passedGates"]) | set(
563
+ review_record["passedGates"]
564
+ )
565
+ missing_gates = sorted(
566
+ set(batch["requiredVerifications"]) - passed_gates
567
+ )
568
+ if missing_gates:
569
+ raise ValueError(
570
+ "FIXBACK_REQUIRED_GATES_MISSING: " + ",".join(missing_gates)
571
+ )
572
+ closed_at = now_iso()
573
+ batch.update(
574
+ {
575
+ "status": "CLOSED",
576
+ "closedAt": closed_at,
577
+ "updatedAt": closed_at,
578
+ "finalProductIdentity": final_product_identity,
579
+ "receipts": {
580
+ "affected": affected_record,
581
+ "review": review_record,
582
+ },
583
+ "verificationRuns": {
584
+ "affected": 1,
585
+ "review": 1,
586
+ },
587
+ }
588
+ )
589
+ _write_json(_batch_path(change_dir, batch_id), batch)
590
+ return batch
591
+
592
+
593
+ def batch_status(change_dir: Path, batch_id: str) -> dict[str, Any]:
594
+ return _load(change_dir, batch_id)
595
+
596
+
597
+ def freeze_readiness(
598
+ change_dir: Path,
599
+ *,
600
+ product_identity: str,
601
+ ) -> dict[str, Any]:
602
+ batches = _all_batches(change_dir)
603
+ opened = [item for item in batches if item.get("status") == "OPEN"]
604
+ if opened:
605
+ return {
606
+ "ok": False,
607
+ "code": "FIXBACK_BATCH_OPEN",
608
+ "batchIds": [item.get("batchId") for item in opened],
609
+ }
610
+ closed = sorted(
611
+ (
612
+ item for item in batches
613
+ if item.get("status") == "CLOSED"
614
+ ),
615
+ key=lambda item: str(item.get("closedAt") or ""),
616
+ )
617
+ if closed and closed[-1].get("finalProductIdentity") != product_identity:
618
+ return {
619
+ "ok": False,
620
+ "code": "FIXBACK_IDENTITY_CHANGED_AFTER_CLOSE",
621
+ "batchId": closed[-1].get("batchId"),
622
+ "expected": closed[-1].get("finalProductIdentity"),
623
+ "actual": product_identity,
624
+ }
625
+ for batch in closed:
626
+ records = [
627
+ batch.get("receipts", {}).get("affected"),
628
+ batch.get("receipts", {}).get("review"),
629
+ ]
630
+ for issue in batch.get("issues", []):
631
+ records.extend(
632
+ [issue.get("redEvidence"), issue.get("greenEvidence")]
633
+ )
634
+ if any(not _evidence_digest_matches(record) for record in records):
635
+ return {
636
+ "ok": False,
637
+ "code": "FIXBACK_EVIDENCE_CHANGED",
638
+ "batchId": batch.get("batchId"),
639
+ }
640
+ return {
641
+ "ok": True,
642
+ "code": "FIXBACK_READY_FOR_FREEZE",
643
+ "closedBatchCount": len(closed),
644
+ }
645
+
646
+
647
+ def _emit(value: dict[str, Any]) -> int:
648
+ print(json.dumps(value, ensure_ascii=False, indent=2))
649
+ return 0 if value.get("ok", True) else 1
650
+
651
+
652
+ def build_parser() -> argparse.ArgumentParser:
653
+ parser = argparse.ArgumentParser(prog="harness_fixback.py")
654
+ sub = parser.add_subparsers(dest="command", required=True)
655
+ opened = sub.add_parser("open")
656
+ opened.add_argument("--change-dir", required=True)
657
+ opened.add_argument("--batch-id", required=True)
658
+ opened.add_argument("--product-identity", required=True)
659
+ opened.add_argument("--root-cause", required=True)
660
+ issue = sub.add_parser("add-issue")
661
+ issue.add_argument("--change-dir", required=True)
662
+ issue.add_argument("--batch-id", required=True)
663
+ issue.add_argument("--issue-id", required=True)
664
+ issue.add_argument("--summary", required=True)
665
+ issue.add_argument("--risk-tag", action="append", default=[])
666
+ resolve = sub.add_parser("resolve-issue")
667
+ resolve.add_argument("--change-dir", required=True)
668
+ resolve.add_argument("--batch-id", required=True)
669
+ resolve.add_argument("--issue-id", required=True)
670
+ resolve.add_argument("--red-evidence", required=True)
671
+ resolve.add_argument("--green-evidence", required=True)
672
+ resolve.add_argument("--changed-file", action="append", default=[])
673
+ close = sub.add_parser("close")
674
+ close.add_argument("--change-dir", required=True)
675
+ close.add_argument("--batch-id", required=True)
676
+ close.add_argument("--final-product-identity", required=True)
677
+ close.add_argument("--affected-receipt", required=True)
678
+ close.add_argument("--review-receipt", required=True)
679
+ status = sub.add_parser("status")
680
+ status.add_argument("--change-dir", required=True)
681
+ status.add_argument("--batch-id", required=True)
682
+ freeze = sub.add_parser("freeze-readiness")
683
+ freeze.add_argument("--change-dir", required=True)
684
+ freeze.add_argument("--product-identity", required=True)
685
+ register = sub.add_parser("register-evidence")
686
+ register.add_argument("--change-dir", required=True)
687
+ register.add_argument("--evidence", required=True)
688
+ return parser
689
+
690
+
691
+ def main(argv: list[str] | None = None) -> int:
692
+ args = build_parser().parse_args(argv)
693
+ change_dir = Path(args.change_dir)
694
+ try:
695
+ if args.command == "open":
696
+ value = open_batch(
697
+ change_dir,
698
+ batch_id=args.batch_id,
699
+ product_identity=args.product_identity,
700
+ root_cause=args.root_cause,
701
+ )
702
+ elif args.command == "add-issue":
703
+ value = add_issue(
704
+ change_dir,
705
+ batch_id=args.batch_id,
706
+ issue_id=args.issue_id,
707
+ summary=args.summary,
708
+ risk_tags=args.risk_tag,
709
+ )
710
+ elif args.command == "resolve-issue":
711
+ value = resolve_issue(
712
+ change_dir,
713
+ batch_id=args.batch_id,
714
+ issue_id=args.issue_id,
715
+ red_evidence=args.red_evidence,
716
+ green_evidence=args.green_evidence,
717
+ changed_files=args.changed_file,
718
+ )
719
+ elif args.command == "close":
720
+ value = close_batch(
721
+ change_dir,
722
+ batch_id=args.batch_id,
723
+ final_product_identity=args.final_product_identity,
724
+ affected_receipt=args.affected_receipt,
725
+ review_receipt=args.review_receipt,
726
+ )
727
+ elif args.command == "status":
728
+ value = batch_status(change_dir, args.batch_id)
729
+ elif args.command == "register-evidence":
730
+ value = register_evidence(change_dir, args.evidence)
731
+ else:
732
+ value = freeze_readiness(
733
+ change_dir,
734
+ product_identity=args.product_identity,
735
+ )
736
+ except (OSError, ValueError) as exc:
737
+ return _emit({"ok": False, "code": str(exc).split(":", 1)[0], "error": str(exc)})
738
+ return _emit(value)
739
+
740
+
741
+ if __name__ == "__main__":
742
+ raise SystemExit(main())