@hunter-harness/workflow-harness 0.2.33 → 0.2.35

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 (243) 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 +3 -3
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  15. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-test/SKILL.md +4 -4
  17. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -0
  18. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -1
  19. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -2
  20. package/harness/bundles/general/claude-code/scripts/harness_archive.py +415 -15
  21. package/harness/bundles/general/claude-code/scripts/harness_context.py +717 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_gate.py +1 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +77 -9
  24. package/harness/bundles/general/claude-code/scripts/harness_phase.py +1 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_aggregate.py +249 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_profile.py +272 -15
  27. package/harness/bundles/general/claude-code/scripts/harness_report_model.py +420 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_review.py +16 -1
  29. package/harness/bundles/general/claude-code/scripts/harness_verification.py +114 -0
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -0
  32. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
  34. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -3
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
  42. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  43. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +4 -4
  45. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -0
  46. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -1
  47. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -2
  48. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +415 -15
  49. package/harness/bundles/general/codebuddy/scripts/harness_context.py +717 -0
  50. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1 -0
  51. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +77 -9
  52. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +1 -0
  53. package/harness/bundles/general/codebuddy/scripts/harness_plan_aggregate.py +249 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +272 -15
  55. package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +420 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_review.py +16 -1
  57. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +114 -0
  58. package/harness/bundles/general/codex/.harness-build.json +1 -1
  59. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -0
  60. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +215 -106
  62. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +8 -0
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  66. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -3
  68. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  69. package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
  70. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  71. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-test/SKILL.md +4 -4
  73. package/harness/bundles/general/codex/harness-test/checklist.md +1 -0
  74. package/harness/bundles/general/codex/harness-test/reference.md +2 -1
  75. package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -2
  76. package/harness/bundles/general/codex/scripts/harness_archive.py +415 -15
  77. package/harness/bundles/general/codex/scripts/harness_context.py +717 -0
  78. package/harness/bundles/general/codex/scripts/harness_gate.py +1 -0
  79. package/harness/bundles/general/codex/scripts/harness_ledger.py +77 -9
  80. package/harness/bundles/general/codex/scripts/harness_phase.py +1 -0
  81. package/harness/bundles/general/codex/scripts/harness_plan_aggregate.py +249 -0
  82. package/harness/bundles/general/codex/scripts/harness_profile.py +272 -15
  83. package/harness/bundles/general/codex/scripts/harness_report_model.py +420 -0
  84. package/harness/bundles/general/codex/scripts/harness_review.py +16 -1
  85. package/harness/bundles/general/codex/scripts/harness_verification.py +114 -0
  86. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  87. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -0
  88. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +215 -106
  90. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +8 -0
  91. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  94. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -3
  96. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  97. package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
  98. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  99. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-test/SKILL.md +4 -4
  101. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -0
  102. package/harness/bundles/general/cursor/harness-test/reference.md +2 -1
  103. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -2
  104. package/harness/bundles/general/cursor/scripts/harness_archive.py +415 -15
  105. package/harness/bundles/general/cursor/scripts/harness_context.py +717 -0
  106. package/harness/bundles/general/cursor/scripts/harness_gate.py +1 -0
  107. package/harness/bundles/general/cursor/scripts/harness_ledger.py +77 -9
  108. package/harness/bundles/general/cursor/scripts/harness_phase.py +1 -0
  109. package/harness/bundles/general/cursor/scripts/harness_plan_aggregate.py +249 -0
  110. package/harness/bundles/general/cursor/scripts/harness_profile.py +272 -15
  111. package/harness/bundles/general/cursor/scripts/harness_report_model.py +420 -0
  112. package/harness/bundles/general/cursor/scripts/harness_review.py +16 -1
  113. package/harness/bundles/general/cursor/scripts/harness_verification.py +114 -0
  114. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  115. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -0
  116. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  118. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +215 -106
  119. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +8 -0
  120. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  123. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -3
  126. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  127. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  128. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
  129. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-test/SKILL.md +4 -4
  131. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -0
  132. package/harness/bundles/java/claude-code/harness-test/reference.md +2 -0
  133. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -2
  134. package/harness/bundles/java/claude-code/scripts/harness_archive.py +415 -15
  135. package/harness/bundles/java/claude-code/scripts/harness_context.py +717 -0
  136. package/harness/bundles/java/claude-code/scripts/harness_gate.py +1 -0
  137. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +77 -9
  138. package/harness/bundles/java/claude-code/scripts/harness_phase.py +1 -0
  139. package/harness/bundles/java/claude-code/scripts/harness_plan_aggregate.py +249 -0
  140. package/harness/bundles/java/claude-code/scripts/harness_profile.py +272 -15
  141. package/harness/bundles/java/claude-code/scripts/harness_report_model.py +420 -0
  142. package/harness/bundles/java/claude-code/scripts/harness_review.py +16 -1
  143. package/harness/bundles/java/claude-code/scripts/harness_verification.py +114 -0
  144. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  145. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -0
  146. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  148. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +215 -106
  149. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +8 -0
  150. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  153. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -3
  156. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  157. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  158. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
  159. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +4 -4
  161. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -0
  162. package/harness/bundles/java/codebuddy/harness-test/reference.md +2 -0
  163. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -2
  164. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +415 -15
  165. package/harness/bundles/java/codebuddy/scripts/harness_context.py +717 -0
  166. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1 -0
  167. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +77 -9
  168. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +1 -0
  169. package/harness/bundles/java/codebuddy/scripts/harness_plan_aggregate.py +249 -0
  170. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +272 -15
  171. package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +420 -0
  172. package/harness/bundles/java/codebuddy/scripts/harness_review.py +16 -1
  173. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +114 -0
  174. package/harness/bundles/java/codex/.harness-build.json +1 -1
  175. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -0
  176. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +215 -106
  179. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +8 -0
  180. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  181. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  183. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -3
  186. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  187. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  188. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
  189. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-test/SKILL.md +4 -4
  191. package/harness/bundles/java/codex/harness-test/checklist.md +1 -0
  192. package/harness/bundles/java/codex/harness-test/reference.md +2 -0
  193. package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -2
  194. package/harness/bundles/java/codex/scripts/harness_archive.py +415 -15
  195. package/harness/bundles/java/codex/scripts/harness_context.py +717 -0
  196. package/harness/bundles/java/codex/scripts/harness_gate.py +1 -0
  197. package/harness/bundles/java/codex/scripts/harness_ledger.py +77 -9
  198. package/harness/bundles/java/codex/scripts/harness_phase.py +1 -0
  199. package/harness/bundles/java/codex/scripts/harness_plan_aggregate.py +249 -0
  200. package/harness/bundles/java/codex/scripts/harness_profile.py +272 -15
  201. package/harness/bundles/java/codex/scripts/harness_report_model.py +420 -0
  202. package/harness/bundles/java/codex/scripts/harness_review.py +16 -1
  203. package/harness/bundles/java/codex/scripts/harness_verification.py +114 -0
  204. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  205. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -0
  206. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  207. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +215 -106
  209. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +8 -0
  210. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  211. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  212. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +389 -117
  213. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  214. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -3
  216. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  217. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  218. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
  219. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-test/SKILL.md +4 -4
  221. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -0
  222. package/harness/bundles/java/cursor/harness-test/reference.md +2 -0
  223. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -2
  224. package/harness/bundles/java/cursor/scripts/harness_archive.py +415 -15
  225. package/harness/bundles/java/cursor/scripts/harness_context.py +717 -0
  226. package/harness/bundles/java/cursor/scripts/harness_gate.py +1 -0
  227. package/harness/bundles/java/cursor/scripts/harness_ledger.py +77 -9
  228. package/harness/bundles/java/cursor/scripts/harness_phase.py +1 -0
  229. package/harness/bundles/java/cursor/scripts/harness_plan_aggregate.py +249 -0
  230. package/harness/bundles/java/cursor/scripts/harness_profile.py +272 -15
  231. package/harness/bundles/java/cursor/scripts/harness_report_model.py +420 -0
  232. package/harness/bundles/java/cursor/scripts/harness_review.py +16 -1
  233. package/harness/bundles/java/cursor/scripts/harness_verification.py +114 -0
  234. package/harness/manifests/general/claude-code.json +42 -26
  235. package/harness/manifests/general/codebuddy.json +42 -26
  236. package/harness/manifests/general/codex.json +42 -26
  237. package/harness/manifests/general/cursor.json +42 -26
  238. package/harness/manifests/java/claude-code.json +44 -28
  239. package/harness/manifests/java/codebuddy.json +44 -28
  240. package/harness/manifests/java/codex.json +44 -28
  241. package/harness/manifests/java/cursor.json +44 -28
  242. package/hunter-workflow-family.json +4 -4
  243. package/package.json +1 -1
@@ -77,6 +77,7 @@ VALIDATION_DEPENDENCIES: dict[str, tuple[str, ...]] = {
77
77
  "unitTest": ("compile",),
78
78
  "unitTestFull": ("unitTest",),
79
79
  "apiTest": ("unitTest",),
80
+ "browserTest": ("unitTest",),
80
81
  "dbCompatibility": ("unitTest",),
81
82
  "package": ("unitTestFull", "apiTest", "dbCompatibility"),
82
83
  }
@@ -43,6 +43,7 @@ VERIFICATIONS = frozenset(
43
43
  "unitTest",
44
44
  "unitTestFull",
45
45
  "apiTest",
46
+ "browserTest",
46
47
  "install",
47
48
  "package",
48
49
  "dbCompatibility",
@@ -72,6 +73,7 @@ REQUIRED_COVERAGE = {
72
73
  "unitTestFull": 1, # module or broader
73
74
  "compile": 1,
74
75
  "apiTest": 1,
76
+ "browserTest": 1,
75
77
  "install": 2, # module-am or broader
76
78
  "package": 2,
77
79
  "dbCompatibility": 1,
@@ -784,6 +786,7 @@ _METRICS_SCHEMAS: dict[str, dict[str, tuple[str, ...]]] = {
784
786
  "unitTest": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
785
787
  "unitTestFull": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
786
788
  "apiTest": {"required": ("total", "passed", "failed"), "optional": ("blocked",)},
789
+ "browserTest": {"required": ("total", "passed", "failed"), "optional": ("skipped", "retries")},
787
790
  "apiContract": {"required": ("scenariosTotal", "passed", "failed"), "optional": ("blocked",)},
788
791
  "browserE2E": {"required": ("total", "passed", "failed"), "optional": ("skipped", "retries")},
789
792
  }
@@ -1086,7 +1089,22 @@ def decide_can_reuse(
1086
1089
  }
1087
1090
 
1088
1091
  validations = ledger.get("validations")
1089
- if not isinstance(validations, dict):
1092
+ targets_raw = ledger.get("verificationTargets")
1093
+ targets = (
1094
+ list(targets_raw.values())
1095
+ if isinstance(targets_raw, dict)
1096
+ else targets_raw
1097
+ if isinstance(targets_raw, list)
1098
+ else []
1099
+ )
1100
+ target_entries = [
1101
+ target
1102
+ for target in targets
1103
+ if isinstance(target, dict) and target.get("verification") == verification
1104
+ ]
1105
+ target_entries.sort(key=lambda item: str(item.get("finishedAt") or ""))
1106
+ entry = target_entries[-1] if target_entries else None
1107
+ if not isinstance(validations, dict) and entry is None:
1090
1108
  return {
1091
1109
  "ok": True,
1092
1110
  "reuse": False,
@@ -1097,7 +1115,8 @@ def decide_can_reuse(
1097
1115
  "ledger_path": str(ledger_path) if ledger_path else None,
1098
1116
  }
1099
1117
 
1100
- entry = validations.get(verification)
1118
+ if entry is None and isinstance(validations, dict):
1119
+ entry = validations.get(verification)
1101
1120
  if not isinstance(entry, dict):
1102
1121
  return {
1103
1122
  "ok": True,
@@ -1118,6 +1137,17 @@ def decide_can_reuse(
1118
1137
  algorithm_version = entry.get("algorithmVersion")
1119
1138
  coverage = entry.get("coverage")
1120
1139
 
1140
+ if entry.get("reusable") is False or isinstance(entry.get("invalidation"), dict):
1141
+ return {
1142
+ "ok": True,
1143
+ "reuse": False,
1144
+ "reason": "rerun",
1145
+ "code": "EVIDENCE_INVALIDATED",
1146
+ "verification": verification,
1147
+ "detail": "recorded target was invalidated by workflow fixback",
1148
+ "invalidation": entry.get("invalidation"),
1149
+ }
1150
+
1121
1151
  # v2 fields: a v1 entry (no algorithmVersion/coverage) is conservatively
1122
1152
  # invalidated once and must be re-recorded with v2 fields (COM-002). No
1123
1153
  # silent upgrade of stale evidence.
@@ -1204,12 +1234,17 @@ def decide_can_reuse(
1204
1234
  ("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
1205
1235
  ):
1206
1236
  stored = entry.get(field)
1207
- if (
1208
- requested
1209
- and str(requested).strip()
1210
- and _nonempty_str(stored)
1211
- and str(stored).strip() != str(requested).strip()
1212
- ):
1237
+ if requested and str(requested).strip() and not _nonempty_str(stored):
1238
+ return {
1239
+ "ok": True,
1240
+ "reuse": False,
1241
+ "reason": "insufficient-evidence",
1242
+ "code": "MISSING_TARGET_IDENTITY",
1243
+ "verification": verification,
1244
+ "detail": f"{field} missing from recorded target",
1245
+ "field": field,
1246
+ }
1247
+ if requested and str(requested).strip() and str(stored).strip() != str(requested).strip():
1213
1248
  return {
1214
1249
  "ok": True,
1215
1250
  "reuse": False,
@@ -1608,7 +1643,40 @@ def cmd_record(args: argparse.Namespace) -> int:
1608
1643
  # Fresh record without scenario-ids must not keep stale ids from prev.
1609
1644
  entry.pop("scenarioIds", None)
1610
1645
 
1611
- ledger["validations"][verification] = entry
1646
+ target_identity = {
1647
+ "verification": verification,
1648
+ "command": entry.get("command"),
1649
+ "scope": entry.get("scope"),
1650
+ "coverage": entry.get("coverage"),
1651
+ "inputsHash": entry.get("inputsHash"),
1652
+ "toolchainHash": entry.get("toolchainHash"),
1653
+ "profileHash": entry.get("profileHash"),
1654
+ "environmentHash": entry.get("environmentHash"),
1655
+ "dbSchemaHash": entry.get("dbSchemaHash"),
1656
+ }
1657
+ target_id = (
1658
+ verification
1659
+ + "-"
1660
+ + hashlib.sha256(
1661
+ json.dumps(
1662
+ target_identity,
1663
+ ensure_ascii=False,
1664
+ sort_keys=True,
1665
+ separators=(",", ":"),
1666
+ ).encode("utf-8")
1667
+ ).hexdigest()[:16]
1668
+ )
1669
+ target = {"id": target_id, "verification": verification, **entry}
1670
+ targets = ledger.setdefault("verificationTargets", {})
1671
+ if not isinstance(targets, dict):
1672
+ targets = {}
1673
+ ledger["verificationTargets"] = targets
1674
+ targets[target_id] = target
1675
+ ledger["validations"][verification] = {
1676
+ key: value
1677
+ for key, value in target.items()
1678
+ if key not in {"id", "verification"}
1679
+ }
1612
1680
  if "changeName" not in ledger:
1613
1681
  ledger["changeName"] = change_dir.name
1614
1682
  if "stateDir" not in ledger:
@@ -39,6 +39,7 @@ VALIDATION_PHASES = {
39
39
  "unitTest": "run",
40
40
  "unitTestFull": "test",
41
41
  "apiTest": "test",
42
+ "browserTest": "test",
42
43
  "dbCompatibility": "test",
43
44
  "package": "package",
44
45
  }
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """Aggregate-plan scale, ownership and dependency-graph contracts."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import fnmatch
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def analyze_scale(payload: dict[str, Any]) -> dict[str, Any]:
14
+ file_count = int(payload.get("fileCount") or 0)
15
+ task_count = int(payload.get("taskCount") or 0)
16
+ module_count = len(
17
+ {str(item) for item in payload.get("modules", []) if str(item).strip()}
18
+ )
19
+ reasons: list[str] = []
20
+ if file_count >= 30:
21
+ reasons.append("file-count")
22
+ if task_count >= 20:
23
+ reasons.append("task-count")
24
+ if module_count >= 3:
25
+ reasons.append("module-count")
26
+ return {
27
+ "ok": True,
28
+ "code": "PLAN_SCALE_ANALYZED",
29
+ "mode": "aggregate" if reasons else "single",
30
+ "reasons": reasons,
31
+ "measurements": {
32
+ "fileCount": file_count,
33
+ "taskCount": task_count,
34
+ "moduleCount": module_count,
35
+ },
36
+ "thresholds": {"fileCount": 30, "taskCount": 20, "moduleCount": 3},
37
+ }
38
+
39
+
40
+ def _literal_prefix(pattern: str) -> str:
41
+ normalized = pattern.replace("\\", "/").strip("/")
42
+ wildcard = min(
43
+ [index for token in ("*", "?", "[") if (index := normalized.find(token)) >= 0]
44
+ or [len(normalized)]
45
+ )
46
+ return normalized[:wildcard].rstrip("/")
47
+
48
+
49
+ def _paths_overlap(left: str, right: str) -> bool:
50
+ left_prefix = _literal_prefix(left)
51
+ right_prefix = _literal_prefix(right)
52
+ if not left_prefix or not right_prefix:
53
+ return True
54
+ if (
55
+ left_prefix == right_prefix
56
+ or left_prefix.startswith(right_prefix + "/")
57
+ or right_prefix.startswith(left_prefix + "/")
58
+ ):
59
+ return True
60
+ return fnmatch.fnmatch(left_prefix, right) or fnmatch.fnmatch(right_prefix, left)
61
+
62
+
63
+ def _cycle(children: list[dict[str, Any]]) -> list[str]:
64
+ ids = {str(child.get("id") or "") for child in children}
65
+ graph = {
66
+ str(child.get("id") or ""): [
67
+ str(dep) for dep in child.get("dependsOn", []) if str(dep) in ids
68
+ ]
69
+ for child in children
70
+ }
71
+ visiting: set[str] = set()
72
+ visited: set[str] = set()
73
+
74
+ def visit(node: str, trail: list[str]) -> list[str]:
75
+ if node in visiting:
76
+ return trail[trail.index(node) :] + [node]
77
+ if node in visited:
78
+ return []
79
+ visiting.add(node)
80
+ for dependency in graph.get(node, []):
81
+ found = visit(dependency, trail + [dependency])
82
+ if found:
83
+ return found
84
+ visiting.remove(node)
85
+ visited.add(node)
86
+ return []
87
+
88
+ for child_id in sorted(ids):
89
+ found = visit(child_id, [child_id])
90
+ if found:
91
+ return found
92
+ return []
93
+
94
+
95
+ def validate_aggregate(plan: dict[str, Any]) -> dict[str, Any]:
96
+ children = [
97
+ child for child in plan.get("children", []) if isinstance(child, dict)
98
+ ]
99
+ ids = [str(child.get("id") or "") for child in children]
100
+ if not children or any(not child_id for child_id in ids) or len(set(ids)) != len(ids):
101
+ return {
102
+ "ok": False,
103
+ "code": "AGGREGATE_CHILD_ID_INVALID",
104
+ "parallelEligible": False,
105
+ }
106
+ cycle = _cycle(children)
107
+ if cycle:
108
+ return {
109
+ "ok": False,
110
+ "code": "AGGREGATE_DAG_CYCLE",
111
+ "cycle": cycle,
112
+ "parallelEligible": False,
113
+ }
114
+ overlaps: list[dict[str, str]] = []
115
+ resource_conflicts: list[dict[str, str]] = []
116
+ for index, left in enumerate(children):
117
+ for right in children[index + 1 :]:
118
+ for left_path in left.get("ownedPaths", []):
119
+ for right_path in right.get("ownedPaths", []):
120
+ if _paths_overlap(str(left_path), str(right_path)):
121
+ overlaps.append(
122
+ {
123
+ "left": str(left["id"]),
124
+ "right": str(right["id"]),
125
+ "leftPath": str(left_path),
126
+ "rightPath": str(right_path),
127
+ }
128
+ )
129
+ left_writes = {
130
+ str(item)
131
+ for item in left.get("sharedResources", {}).get("writes", [])
132
+ }
133
+ right_writes = {
134
+ str(item)
135
+ for item in right.get("sharedResources", {}).get("writes", [])
136
+ }
137
+ for resource in sorted(left_writes & right_writes):
138
+ resource_conflicts.append(
139
+ {
140
+ "left": str(left["id"]),
141
+ "right": str(right["id"]),
142
+ "resource": resource,
143
+ }
144
+ )
145
+ if overlaps:
146
+ return {
147
+ "ok": False,
148
+ "code": "AGGREGATE_OWNERSHIP_OVERLAP",
149
+ "parallelEligible": False,
150
+ "overlaps": overlaps,
151
+ }
152
+ if resource_conflicts:
153
+ return {
154
+ "ok": False,
155
+ "code": "AGGREGATE_RESOURCE_CONFLICT",
156
+ "parallelEligible": False,
157
+ "conflicts": resource_conflicts,
158
+ }
159
+ integration = [child for child in children if child.get("kind") == "integration"]
160
+ leaf_ids = {str(child["id"]) for child in children if child.get("kind") != "integration"}
161
+ if integration:
162
+ dependencies = {str(item) for item in integration[-1].get("dependsOn", [])}
163
+ if not leaf_ids.issubset(dependencies):
164
+ return {
165
+ "ok": False,
166
+ "code": "AGGREGATE_INTEGRATION_CLOSURE_MISSING",
167
+ "parallelEligible": False,
168
+ "missingChildren": sorted(leaf_ids - dependencies),
169
+ }
170
+ receipts = {
171
+ str(receipt.get("childId") or ""): receipt
172
+ for receipt in plan.get("receipts", [])
173
+ if isinstance(receipt, dict)
174
+ }
175
+ if receipts and integration:
176
+ leaf_commits = {
177
+ str(receipts.get(child_id, {}).get("productCommit") or "")
178
+ for child_id in leaf_ids
179
+ }
180
+ leaf_commits.discard("")
181
+ integration_receipt = receipts.get(str(integration[-1]["id"]), {})
182
+ covered = {
183
+ str(item) for item in integration_receipt.get("coversProductCommits", [])
184
+ }
185
+ if leaf_commits != covered:
186
+ return {
187
+ "ok": False,
188
+ "code": "AGGREGATE_PRODUCT_COMMIT_CLOSURE_MISSING",
189
+ "parallelEligible": False,
190
+ "missingProductCommits": sorted(leaf_commits - covered),
191
+ }
192
+ return {
193
+ "ok": True,
194
+ "code": "AGGREGATE_PLAN_VALID",
195
+ "parallelEligible": True,
196
+ "childOrder": ids,
197
+ "integrationChild": str(integration[-1]["id"]) if integration else None,
198
+ }
199
+
200
+
201
+ def materialize(payload: dict[str, Any]) -> dict[str, Any]:
202
+ scale = analyze_scale(payload)
203
+ children = payload.get("children") or payload.get("slices") or []
204
+ result = {
205
+ "schemaVersion": 1,
206
+ "code": "AGGREGATE_PLAN_MATERIALIZED",
207
+ "parent": {
208
+ "id": str(payload.get("id") or payload.get("changeName") or "aggregate"),
209
+ "scaleDecision": scale,
210
+ },
211
+ "children": children,
212
+ "integration": payload.get("integration"),
213
+ }
214
+ validation = validate_aggregate({"children": children})
215
+ result["validation"] = validation
216
+ result["ok"] = validation["ok"]
217
+ return result
218
+
219
+
220
+ def build_parser() -> argparse.ArgumentParser:
221
+ parser = argparse.ArgumentParser(prog="harness_plan_aggregate.py")
222
+ sub = parser.add_subparsers(dest="command", required=True)
223
+ for command in ("analyze", "validate", "materialize"):
224
+ child = sub.add_parser(command)
225
+ child.add_argument("--input", required=True, type=Path)
226
+ child.add_argument("--out", type=Path)
227
+ child.add_argument("--json", action="store_true")
228
+ return parser
229
+
230
+
231
+ def main(argv: list[str] | None = None) -> int:
232
+ args = build_parser().parse_args(argv)
233
+ payload = json.loads(args.input.read_text(encoding="utf-8-sig"))
234
+ result = (
235
+ analyze_scale(payload)
236
+ if args.command == "analyze"
237
+ else validate_aggregate(payload)
238
+ if args.command == "validate"
239
+ else materialize(payload)
240
+ )
241
+ if args.out:
242
+ args.out.parent.mkdir(parents=True, exist_ok=True)
243
+ args.out.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
244
+ print(json.dumps(result, ensure_ascii=False, indent=2))
245
+ return 0 if result.get("ok", True) else 1
246
+
247
+
248
+ if __name__ == "__main__":
249
+ raise SystemExit(main())