@hunter-harness/workflow-harness 0.2.1 → 0.2.2

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/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +2 -8
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -1
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  13. package/harness/bundles/general/claude-code/harness-submit/checklist.md +30 -6
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
  16. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/pitfalls.md +16 -0
  18. package/harness/bundles/general/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  19. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +1 -1
  20. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +55 -2
  21. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +1 -8
  22. package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +13 -0
  23. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_archive.py +31 -14
  25. package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +103 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +179 -4
  27. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +269 -3
  28. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +72 -382
  29. package/harness/bundles/general/claude-code/scripts/harness_profile.py +742 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_service.py +99 -2
  31. package/harness/bundles/general/claude-code/scripts/harness_state.py +179 -0
  32. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  33. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-archive/reference.md +2 -8
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -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 +474 -122
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -1
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  44. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +30 -6
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
  47. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +16 -0
  49. package/harness/bundles/general/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  50. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +1 -1
  51. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +55 -2
  52. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  53. package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  54. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +31 -14
  56. package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +103 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +179 -4
  58. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +269 -3
  59. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +72 -382
  60. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +742 -0
  61. package/harness/bundles/general/codebuddy/scripts/harness_service.py +99 -2
  62. package/harness/bundles/general/codebuddy/scripts/harness_state.py +179 -0
  63. package/harness/bundles/general/codex/.harness-build.json +1 -1
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-archive/reference.md +2 -8
  66. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -1
  67. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  69. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -1
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  74. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  75. package/harness/bundles/general/codex/harness-submit/checklist.md +30 -6
  76. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
  78. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  79. package/harness/bundles/general/codex/harness-test/pitfalls.md +16 -0
  80. package/harness/bundles/general/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  81. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +1 -1
  82. package/harness/bundles/general/codex/protocols/ledger-protocol.md +55 -2
  83. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +1 -8
  84. package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +13 -0
  85. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -0
  86. package/harness/bundles/general/codex/scripts/harness_archive.py +31 -14
  87. package/harness/bundles/general/codex/scripts/harness_check_gate.py +103 -0
  88. package/harness/bundles/general/codex/scripts/harness_deploy.py +179 -4
  89. package/harness/bundles/general/codex/scripts/harness_ledger.py +269 -3
  90. package/harness/bundles/general/codex/scripts/harness_preflight.py +72 -382
  91. package/harness/bundles/general/codex/scripts/harness_profile.py +742 -0
  92. package/harness/bundles/general/codex/scripts/harness_service.py +99 -2
  93. package/harness/bundles/general/codex/scripts/harness_state.py +179 -0
  94. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  95. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-archive/reference.md +2 -8
  97. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -1
  98. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  100. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
  102. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -1
  103. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  106. package/harness/bundles/general/cursor/harness-submit/checklist.md +30 -6
  107. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
  109. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  110. package/harness/bundles/general/cursor/harness-test/pitfalls.md +16 -0
  111. package/harness/bundles/general/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  112. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +1 -1
  113. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +55 -2
  114. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +1 -8
  115. package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +13 -0
  116. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -0
  117. package/harness/bundles/general/cursor/scripts/harness_archive.py +31 -14
  118. package/harness/bundles/general/cursor/scripts/harness_check_gate.py +103 -0
  119. package/harness/bundles/general/cursor/scripts/harness_deploy.py +179 -4
  120. package/harness/bundles/general/cursor/scripts/harness_ledger.py +269 -3
  121. package/harness/bundles/general/cursor/scripts/harness_preflight.py +72 -382
  122. package/harness/bundles/general/cursor/scripts/harness_profile.py +742 -0
  123. package/harness/bundles/general/cursor/scripts/harness_service.py +99 -2
  124. package/harness/bundles/general/cursor/scripts/harness_state.py +179 -0
  125. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  126. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-archive/reference.md +2 -8
  129. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -1
  130. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  132. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -3
  134. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
  135. package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -1
  136. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  137. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -3
  139. package/harness/bundles/java/claude-code/harness-submit/checklist.md +30 -6
  140. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -5
  142. package/harness/bundles/java/claude-code/harness-test/pitfalls.md +16 -0
  143. package/harness/bundles/java/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  144. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +1 -1
  145. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +55 -2
  146. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +1 -8
  147. package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +13 -0
  148. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -0
  149. package/harness/bundles/java/claude-code/scripts/harness_archive.py +31 -14
  150. package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +103 -0
  151. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +179 -4
  152. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +269 -3
  153. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +72 -382
  154. package/harness/bundles/java/claude-code/scripts/harness_profile.py +742 -0
  155. package/harness/bundles/java/claude-code/scripts/harness_service.py +99 -2
  156. package/harness/bundles/java/claude-code/scripts/harness_state.py +179 -0
  157. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  158. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-archive/reference.md +2 -8
  161. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -1
  162. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  164. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +3 -3
  166. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
  167. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -1
  168. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  169. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -3
  171. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +30 -6
  172. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -5
  174. package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +16 -0
  175. package/harness/bundles/java/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  176. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +1 -1
  177. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +55 -2
  178. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  179. package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  180. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -0
  181. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +31 -14
  182. package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +103 -0
  183. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +179 -4
  184. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +269 -3
  185. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +72 -382
  186. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +742 -0
  187. package/harness/bundles/java/codebuddy/scripts/harness_service.py +99 -2
  188. package/harness/bundles/java/codebuddy/scripts/harness_state.py +179 -0
  189. package/harness/bundles/java/codex/.harness-build.json +1 -1
  190. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/harness-archive/reference.md +2 -8
  193. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -1
  194. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  196. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-package/SKILL.md +3 -3
  198. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
  199. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -1
  200. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  201. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  202. package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -3
  203. package/harness/bundles/java/codex/harness-submit/checklist.md +30 -6
  204. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  205. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -5
  206. package/harness/bundles/java/codex/harness-test/pitfalls.md +16 -0
  207. package/harness/bundles/java/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  208. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +1 -1
  209. package/harness/bundles/java/codex/protocols/ledger-protocol.md +55 -2
  210. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +1 -8
  211. package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +13 -0
  212. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -0
  213. package/harness/bundles/java/codex/scripts/harness_archive.py +31 -14
  214. package/harness/bundles/java/codex/scripts/harness_check_gate.py +103 -0
  215. package/harness/bundles/java/codex/scripts/harness_deploy.py +179 -4
  216. package/harness/bundles/java/codex/scripts/harness_ledger.py +269 -3
  217. package/harness/bundles/java/codex/scripts/harness_preflight.py +72 -382
  218. package/harness/bundles/java/codex/scripts/harness_profile.py +742 -0
  219. package/harness/bundles/java/codex/scripts/harness_service.py +99 -2
  220. package/harness/bundles/java/codex/scripts/harness_state.py +179 -0
  221. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  222. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  223. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-archive/reference.md +2 -8
  225. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -1
  226. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  227. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  228. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-package/SKILL.md +3 -3
  230. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
  231. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -1
  232. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  233. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  234. package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -3
  235. package/harness/bundles/java/cursor/harness-submit/checklist.md +30 -6
  236. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  237. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -5
  238. package/harness/bundles/java/cursor/harness-test/pitfalls.md +16 -0
  239. package/harness/bundles/java/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  240. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +1 -1
  241. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +55 -2
  242. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +1 -8
  243. package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +13 -0
  244. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -0
  245. package/harness/bundles/java/cursor/scripts/harness_archive.py +31 -14
  246. package/harness/bundles/java/cursor/scripts/harness_check_gate.py +103 -0
  247. package/harness/bundles/java/cursor/scripts/harness_deploy.py +179 -4
  248. package/harness/bundles/java/cursor/scripts/harness_ledger.py +269 -3
  249. package/harness/bundles/java/cursor/scripts/harness_preflight.py +72 -382
  250. package/harness/bundles/java/cursor/scripts/harness_profile.py +742 -0
  251. package/harness/bundles/java/cursor/scripts/harness_service.py +99 -2
  252. package/harness/bundles/java/cursor/scripts/harness_state.py +179 -0
  253. package/harness/manifests/general/claude-code.json +44 -28
  254. package/harness/manifests/general/codebuddy.json +44 -28
  255. package/harness/manifests/general/codex.json +44 -28
  256. package/harness/manifests/general/cursor.json +44 -28
  257. package/harness/manifests/java/claude-code.json +45 -29
  258. package/harness/manifests/java/codebuddy.json +45 -29
  259. package/harness/manifests/java/codex.json +45 -29
  260. package/harness/manifests/java/cursor.json +45 -29
  261. package/hunter-workflow-family.json +2 -2
  262. package/package.json +1 -1
@@ -0,0 +1,742 @@
1
+ #!/usr/bin/env python3
2
+ """Harness profile v2 — public deterministic profile resolver/validator/migrator.
3
+
4
+ Change cluster 1 of harness-deterministic-performance.
5
+
6
+ Spec §3.1:每个 verification 声明 command/argvTemplate/scope/inputs/coverage/
7
+ source/basis;持久 profile 只保存模板,不含具体 change-name/worktree 路径或已解析
8
+ overlay;运行期 resolve 结果写入 change runtime/session。
9
+
10
+ 兼容性:保留顶层 `verificationInputs`(派生自 commands.<key>.inputs)以兼容
11
+ harness_ledger.py v1 的 expand_profile_input_files,直到 cluster 2 升级 ledger。
12
+
13
+ Python 3.10+ stdlib only. UTF-8 without BOM. Windows path friendly.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import datetime as dt
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import re
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+
31
+ if hasattr(sys.stdout, "reconfigure"):
32
+ sys.stdout.reconfigure(encoding="utf-8")
33
+ if hasattr(sys.stderr, "reconfigure"):
34
+ sys.stderr.reconfigure(encoding="utf-8")
35
+
36
+
37
+ SCHEMA_VERSION = 2
38
+ PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
39
+
40
+ # 排除策略(spec §3.1):所有路径必须位于 project root,排除以下目录。
41
+ # 兄弟 worktree、构建产物、依赖目录、缓存一律不进入 verification inputs。
42
+ DEFAULT_EXCLUDED_ROOTS: tuple[str, ...] = (
43
+ ".git",
44
+ ".harness",
45
+ ".claude/worktrees",
46
+ ".cursor/worktrees",
47
+ ".codeium/worktrees",
48
+ "target",
49
+ "build",
50
+ "dist",
51
+ "node_modules",
52
+ ".gradle",
53
+ ".idea",
54
+ ".vscode",
55
+ "__pycache__",
56
+ ".pytest_cache",
57
+ ".cache",
58
+ )
59
+
60
+ # 覆盖层级(spec §3.2):unitTest ⊂ unitTestFull;package 独立;submit 复用 unitTestFull。
61
+ VERIFICATION_KEYS: tuple[str, ...] = (
62
+ "compile",
63
+ "unitTest",
64
+ "unitTestFull",
65
+ "install",
66
+ "package",
67
+ )
68
+
69
+ VALID_SOURCES = ("detected", "user")
70
+
71
+
72
+ def now_iso() -> str:
73
+ return dt.datetime.now().astimezone().isoformat(timespec="seconds")
74
+
75
+
76
+ def read_json(path: Path) -> Any:
77
+ # utf-8-sig 兼容可能残留的 BOM(与 harness_ledger.py 保持一致)。
78
+ return json.loads(path.read_text(encoding="utf-8-sig"))
79
+
80
+
81
+ def write_json(path: Path, data: Any) -> None:
82
+ path.parent.mkdir(parents=True, exist_ok=True)
83
+ text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
84
+ # 强制 LF,UTF-8 无 BOM(spec §3.4 字节级指纹一致性)。
85
+ # 原子写 temp+os.replace:崩溃后不留半写文件(与 runtime-helpers.mjs writeJsonUtf8NoBom 一致)。
86
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
87
+ try:
88
+ tmp.write_text(text, encoding="utf-8", newline="\n")
89
+ os.replace(tmp, path)
90
+ except BaseException:
91
+ tmp.unlink(missing_ok=True)
92
+ raise
93
+
94
+
95
+ def sha256_file(path: Path) -> str:
96
+ if not path.is_file():
97
+ return ""
98
+ h = hashlib.sha256()
99
+ with path.open("rb") as fh:
100
+ for chunk in iter(lambda: fh.read(65536), b""):
101
+ h.update(chunk)
102
+ return h.hexdigest()
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # 排除策略与路径逃逸校验
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def _excluded_segments(excluded: tuple[str, ...] | list[str]) -> tuple[tuple[str, ...], ...]:
110
+ return tuple(tuple(e.split("/")) for e in excluded)
111
+
112
+
113
+ def is_path_excluded(rel_path: str, excluded: tuple[str, ...] | list[str]) -> bool:
114
+ """rel_path 是否落在某个 excluded root 下。
115
+
116
+ rel_path 用 posix 斜杠;支持多段 excluded root(如 .claude/worktrees)。
117
+ """
118
+ parts = Path(rel_path).parts
119
+ for segs in _excluded_segments(excluded):
120
+ if len(parts) >= len(segs) and parts[: len(segs)] == segs:
121
+ return True
122
+ return False
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # 探测
127
+ # ---------------------------------------------------------------------------
128
+
129
+ def detect_project_type(project: Path) -> str:
130
+ if (project / "pom.xml").is_file():
131
+ return "java-maven"
132
+ if (project / "package.json").is_file():
133
+ return "node"
134
+ return "unknown"
135
+
136
+
137
+ def which_tool(name: str) -> str:
138
+ found = shutil.which(name)
139
+ if found:
140
+ return str(Path(found).resolve())
141
+ if os.name == "nt":
142
+ for ext in (".cmd", ".bat", ".exe"):
143
+ found = shutil.which(name + ext)
144
+ if found:
145
+ return str(Path(found).resolve())
146
+ return ""
147
+
148
+
149
+ def find_reactor_modules(project: Path, excluded: tuple[str, ...] | list[str]) -> list[str]:
150
+ """发现当前 checkout 内含 pom.xml 的模块(repo-relative,排序去重)。
151
+
152
+ 兄弟 worktree、target/build/node_modules 等排除目录内的 POM 一律排除
153
+ (UT-003/UT-004)。root pom → ".";子模块 → "module-a"。
154
+ """
155
+ project = project.resolve()
156
+ modules: set[str] = set()
157
+ for pom in project.rglob("pom.xml"):
158
+ if not pom.is_file():
159
+ continue
160
+ rel = pom.relative_to(project)
161
+ if is_path_excluded(rel.as_posix(), excluded):
162
+ continue
163
+ parent = rel.parent
164
+ mod = "." if str(parent) == "." else parent.as_posix()
165
+ modules.add(mod)
166
+ return sorted(modules)
167
+
168
+
169
+ def _cmd_inputs(
170
+ modules: list[str], *, pom: bool, main: bool, test: bool
171
+ ) -> list[str]:
172
+ """按 reactor modules 生成 verification inputs(排序去重)。"""
173
+ result: list[str] = []
174
+ for m in modules:
175
+ prefix = "" if m == "." else f"{m}/"
176
+ if pom:
177
+ result.append(f"{prefix}pom.xml")
178
+ if main:
179
+ result.append(f"{prefix}src/main/**")
180
+ if test:
181
+ result.append(f"{prefix}src/test/**")
182
+ return sorted(set(result))
183
+
184
+
185
+ def _java_commands(reactor_modules: list[str], pom_hash: str) -> dict[str, Any]:
186
+ basis = {"reactorModules": list(reactor_modules), "pomHash": pom_hash}
187
+ full_inputs = _cmd_inputs(reactor_modules, pom=True, main=True, test=True)
188
+ return {
189
+ "compile": {
190
+ "command": "mvn -f pom.xml compile -o -q",
191
+ "argvTemplate": ["mvn", "-f", "pom.xml", "compile", "-o", "-q"],
192
+ "scope": "module",
193
+ "inputs": _cmd_inputs(reactor_modules, pom=True, main=True, test=False),
194
+ "coverage": "compile",
195
+ "source": "detected",
196
+ "basis": dict(basis),
197
+ },
198
+ "unitTest": {
199
+ "command": "mvn -f pom.xml test -Dtest={testClasses} -o",
200
+ "argvTemplate": ["mvn", "-f", "pom.xml", "test", "-Dtest={testClasses}", "-o"],
201
+ "scope": "incremental",
202
+ "inputs": _cmd_inputs(reactor_modules, pom=True, main=False, test=True),
203
+ "coverage": "unitTest",
204
+ "source": "detected",
205
+ "basis": dict(basis),
206
+ },
207
+ "unitTestFull": {
208
+ "command": "mvn -f pom.xml test -o",
209
+ "argvTemplate": ["mvn", "-f", "pom.xml", "test", "-o"],
210
+ "scope": "full",
211
+ "inputs": full_inputs,
212
+ "coverage": "unitTestFull",
213
+ "source": "detected",
214
+ "basis": dict(basis),
215
+ },
216
+ "install": {
217
+ "command": "mvn install -pl {modules} -am -DskipTests -nsu",
218
+ "argvTemplate": ["mvn", "install", "-pl", "{modules}", "-am", "-DskipTests", "-nsu"],
219
+ "scope": "module-am",
220
+ "inputs": _cmd_inputs(reactor_modules, pom=True, main=False, test=False),
221
+ "coverage": "install",
222
+ "source": "detected",
223
+ "basis": dict(basis),
224
+ },
225
+ "package": {
226
+ "command": "mvn -f pom.xml package '-Dmaven.test.skip=true'",
227
+ "argvTemplate": ["mvn", "-f", "pom.xml", "package", "-Dmaven.test.skip=true"],
228
+ "scope": "module",
229
+ "inputs": _cmd_inputs(reactor_modules, pom=True, main=True, test=True),
230
+ "coverage": "package",
231
+ "source": "detected",
232
+ "basis": dict(basis),
233
+ },
234
+ }
235
+
236
+
237
+ def _node_commands(project: Path) -> dict[str, Any]:
238
+ """commands for a node project: unitTestFull = `npm run check` (or `npm test`)
239
+ with an input closure covering TS sources/tests + config (+ harness Python/.mjs
240
+ when the project dogfoods harness). Lets can-reuse --profile-input unitTestFull
241
+ reuse a green full check instead of forcing insufficient-evidence."""
242
+ pkg = project / "package.json"
243
+ if not pkg.is_file():
244
+ return {}
245
+ try:
246
+ data = json.loads(pkg.read_text(encoding="utf-8-sig"))
247
+ except (OSError, json.JSONDecodeError):
248
+ return {}
249
+ scripts = data.get("scripts") if isinstance(data, dict) else None
250
+ if not isinstance(scripts, dict):
251
+ return {}
252
+ script_key = "check" if "check" in scripts else ("test" if "test" in scripts else None)
253
+ if not script_key:
254
+ return {}
255
+ full_cmd = scripts[script_key]
256
+ # Precise globs (avoid node_modules/** which would make the inputs hash unstable).
257
+ inputs = [
258
+ "package.json",
259
+ "tsconfig.json",
260
+ "tsconfig.*.json",
261
+ "vitest.config.*",
262
+ "eslint.config.*",
263
+ "src/**/*.ts",
264
+ "src/**/*.tsx",
265
+ "test/**/*.ts",
266
+ "test/**/*.tsx",
267
+ "packages/*/src/**/*.ts",
268
+ "packages/*/src/**/*.tsx",
269
+ "packages/*/test/**/*.ts",
270
+ "packages/*/test/**/*.tsx",
271
+ "apps/*/src/**/*.ts",
272
+ "apps/*/src/**/*.tsx",
273
+ "apps/*/test/**/*.ts",
274
+ ]
275
+ # harness dogfood: canonical Python + .mjs sources feed npm run check (vitest
276
+ # imports harness-test/scripts; smoke:pack runs sync-harness.mjs).
277
+ if (project / "harness").is_dir():
278
+ inputs.extend(
279
+ [
280
+ "harness/scripts/*.py",
281
+ "harness/harness-knowledge-ingest/scripts/*.py",
282
+ "harness/harness-test/scripts/*.mjs",
283
+ "harness/harness-test/scripts/tests/*.mjs",
284
+ "scripts/*.mjs",
285
+ ]
286
+ )
287
+ return {
288
+ "unitTestFull": {
289
+ "command": full_cmd,
290
+ "argvTemplate": full_cmd.split(),
291
+ "scope": "full",
292
+ "inputs": inputs,
293
+ "coverage": "unitTestFull",
294
+ "source": "detected",
295
+ "basis": {"packageScript": script_key},
296
+ }
297
+ }
298
+
299
+
300
+ def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, Any]:
301
+ return {
302
+ "schemaVersion": SCHEMA_VERSION,
303
+ "detectedAt": "",
304
+ "projectType": "unknown",
305
+ "toolPaths": {"node": "", "mvn": ""},
306
+ "excludedRoots": list(excluded),
307
+ "commands": {},
308
+ # 兼容字段:cluster 2 升级 ledger 后由 commands 派生;期间保持 v1 可消费。
309
+ "verificationInputs": {},
310
+ "serviceStart": {
311
+ "command": "",
312
+ "healthUrl": "",
313
+ "startTimeoutSec": 120,
314
+ "inputFiles": [],
315
+ "source": "detected",
316
+ "profile": "",
317
+ "overlayPath": "",
318
+ },
319
+ "identifier": {
320
+ "pattern": r"^[A-Za-z][A-Za-z0-9_-]*$",
321
+ "maxLength": 64,
322
+ "prefix": "",
323
+ },
324
+ "knownPreexistingErrors": [],
325
+ "shellQuirks": [],
326
+ "fingerprint": {"mvnVersion": "", "nodeVersion": "", "pomHash": ""},
327
+ }
328
+
329
+
330
+ def load_profile(project: Path) -> dict[str, Any] | None:
331
+ path = project / PROFILE_REL
332
+ if not path.is_file():
333
+ return None
334
+ try:
335
+ data = read_json(path)
336
+ except (OSError, json.JSONDecodeError):
337
+ return None
338
+ return data if isinstance(data, dict) else None
339
+
340
+
341
+ def _merge_user_overrides(
342
+ profile: dict[str, Any], existing: dict[str, Any] | None
343
+ ) -> None:
344
+ """只保留 source=user 的 command 覆写;detected 字段每次 fresh 重建。
345
+
346
+ spec §3.1:user override 显式 source=user 才永久保留;detected basis 过期自动重探测。
347
+ v1 profile 无 provenance 字段,buildCommands 不当 user override 保留(由 migrate 报告)。
348
+ """
349
+ if not existing:
350
+ return
351
+ cmds = existing.get("commands")
352
+ if isinstance(cmds, dict):
353
+ for key, cmd in cmds.items():
354
+ if isinstance(cmd, dict) and cmd.get("source") == "user":
355
+ profile["commands"][key] = dict(cmd)
356
+ if isinstance(existing.get("knownPreexistingErrors"), list):
357
+ profile["knownPreexistingErrors"] = list(existing["knownPreexistingErrors"])
358
+ if isinstance(existing.get("shellQuirks"), list):
359
+ profile["shellQuirks"] = list(existing["shellQuirks"])
360
+ # 兼容字段:用户在 v1 配置的 verificationInputs 不保留(无 provenance),
361
+ # 由 commands 重新派生。knownPreexistingErrors/shellQuirks 是人工标注,保留。
362
+
363
+
364
+ def _derive_verification_inputs(profile: dict[str, Any]) -> None:
365
+ """从 commands 派生 verificationInputs.<key>,兼容 ledger v1 消费。"""
366
+ cmds = profile.get("commands") or {}
367
+ vi: dict[str, list[str]] = {}
368
+ for key, cmd in cmds.items():
369
+ if isinstance(cmd, dict) and isinstance(cmd.get("inputs"), list):
370
+ vi[key] = list(cmd["inputs"])
371
+ profile["verificationInputs"] = vi
372
+
373
+
374
+ def detect(project: Path) -> dict[str, Any]:
375
+ project = project.resolve()
376
+ existing = load_profile(project)
377
+ excluded = DEFAULT_EXCLUDED_ROOTS
378
+ project_type = detect_project_type(project)
379
+
380
+ profile = empty_profile_skeleton(excluded)
381
+ profile["projectType"] = project_type
382
+ profile["detectedAt"] = now_iso()
383
+
384
+ node_path = which_tool("node")
385
+ mvn_path = which_tool("mvn")
386
+ profile["toolPaths"] = {"node": node_path, "mvn": mvn_path}
387
+
388
+ pom_hash = sha256_file(project / "pom.xml") if (project / "pom.xml").is_file() else ""
389
+ profile["fingerprint"] = {"mvnVersion": "", "nodeVersion": "", "pomHash": pom_hash}
390
+
391
+ if project_type == "java-maven":
392
+ reactor_modules = find_reactor_modules(project, excluded)
393
+ if reactor_modules:
394
+ profile["commands"] = _java_commands(reactor_modules, pom_hash)
395
+ elif project_type == "node":
396
+ node_cmds = _node_commands(project)
397
+ if node_cmds:
398
+ profile["commands"] = node_cmds
399
+
400
+ _merge_user_overrides(profile, existing)
401
+ _derive_verification_inputs(profile)
402
+
403
+ out_path = project / PROFILE_REL
404
+ write_json(out_path, profile)
405
+
406
+ return {
407
+ "ok": True,
408
+ "action": "detect",
409
+ "project": str(project),
410
+ "profilePath": str(out_path),
411
+ "profile": profile,
412
+ "created": existing is None,
413
+ "updated": True,
414
+ }
415
+
416
+
417
+ # ---------------------------------------------------------------------------
418
+ # check(missing/invalid/stale/ready)
419
+ # ---------------------------------------------------------------------------
420
+
421
+ def check(project: Path) -> dict[str, Any]:
422
+ project = project.resolve()
423
+ path = project / PROFILE_REL
424
+ hint = "python harness_profile.py detect --project <root> --json"
425
+
426
+ if not path.is_file():
427
+ return {
428
+ "ok": False,
429
+ "action": "check",
430
+ "project": str(project),
431
+ "status": "missing",
432
+ "stale": True,
433
+ "issues": ["build-profile.json missing; run detect"],
434
+ "hint": hint,
435
+ }
436
+
437
+ try:
438
+ profile = read_json(path)
439
+ except (OSError, json.JSONDecodeError) as exc:
440
+ return {
441
+ "ok": False,
442
+ "action": "check",
443
+ "project": str(project),
444
+ "status": "invalid",
445
+ "stale": True,
446
+ "issues": [f"build-profile.json unreadable: {exc}"],
447
+ "hint": hint,
448
+ }
449
+ if not isinstance(profile, dict):
450
+ return {
451
+ "ok": False,
452
+ "action": "check",
453
+ "project": str(project),
454
+ "status": "invalid",
455
+ "stale": True,
456
+ "issues": ["build-profile.json is not an object"],
457
+ "hint": hint,
458
+ }
459
+ if profile.get("schemaVersion") != SCHEMA_VERSION:
460
+ return {
461
+ "ok": False,
462
+ "action": "check",
463
+ "project": str(project),
464
+ "status": "invalid",
465
+ "stale": True,
466
+ "issues": [
467
+ f"schemaVersion={profile.get('schemaVersion')} != {SCHEMA_VERSION}; run migrate"
468
+ ],
469
+ "hint": "python harness_profile.py migrate --project <root> --apply --json",
470
+ }
471
+
472
+ issues: list[str] = []
473
+ stale = False
474
+
475
+ fp = profile.get("fingerprint") or {}
476
+ stored_pom = fp.get("pomHash") or "" if isinstance(fp, dict) else ""
477
+ current_pom = sha256_file(project / "pom.xml") if (project / "pom.xml").is_file() else ""
478
+ if stored_pom != current_pom:
479
+ stale = True
480
+ issues.append(
481
+ f"fingerprint.pomHash changed: stored={stored_pom or '(empty)'} "
482
+ f"current={current_pom or '(empty)'}"
483
+ )
484
+
485
+ # toolPaths 失效检查(空允许,路径不存在则 stale)
486
+ tool_paths = profile.get("toolPaths") or {}
487
+ if isinstance(tool_paths, dict):
488
+ for tool_name in ("node", "mvn"):
489
+ p = tool_paths.get(tool_name) or ""
490
+ if p and not Path(p).exists():
491
+ stale = True
492
+ issues.append(f"toolPaths.{tool_name} missing: {p}")
493
+
494
+ status = "stale" if stale else "ready"
495
+ return {
496
+ "ok": not stale,
497
+ "action": "check",
498
+ "project": str(project),
499
+ "status": status,
500
+ "stale": stale,
501
+ "issues": issues,
502
+ }
503
+
504
+
505
+ # ---------------------------------------------------------------------------
506
+ # validate_profile(路径逃逸 + excluded root)
507
+ # ---------------------------------------------------------------------------
508
+
509
+ def validate_profile(profile: dict[str, Any], project: Path) -> list[str]:
510
+ issues: list[str] = []
511
+ excluded = tuple(profile.get("excludedRoots") or DEFAULT_EXCLUDED_ROOTS)
512
+ commands = profile.get("commands") or {}
513
+ for key, cmd in commands.items():
514
+ if not isinstance(cmd, dict):
515
+ continue
516
+ for inp in cmd.get("inputs") or []:
517
+ if not isinstance(inp, str) or not inp.strip():
518
+ continue
519
+ segments = inp.split("/")
520
+ if ".." in segments:
521
+ issues.append(
522
+ f"command {key} input escapes project root: {inp}"
523
+ )
524
+ elif is_path_excluded(inp, excluded):
525
+ issues.append(
526
+ f"command {key} input in excluded root: {inp}"
527
+ )
528
+ return issues
529
+
530
+
531
+ # ---------------------------------------------------------------------------
532
+ # resolve_command(运行期占位替换,不写回持久 profile)
533
+ # ---------------------------------------------------------------------------
534
+
535
+ def resolve_command(
536
+ profile: dict[str, Any],
537
+ key: str,
538
+ *,
539
+ test_classes: list[str] | None = None,
540
+ modules: list[str] | None = None,
541
+ ) -> dict[str, Any]:
542
+ cmd = (profile.get("commands") or {}).get(key)
543
+ if not isinstance(cmd, dict):
544
+ raise KeyError(f"command '{key}' not found in profile")
545
+ replacements: dict[str, str] = {}
546
+ if test_classes is not None:
547
+ replacements["{testClasses}"] = ",".join(test_classes)
548
+ if modules is not None:
549
+ replacements["{modules}"] = ",".join(modules)
550
+
551
+ command = cmd.get("command", "")
552
+ for placeholder, val in replacements.items():
553
+ command = command.replace(placeholder, val)
554
+
555
+ argv: list[str] = []
556
+ for tok in cmd.get("argvTemplate") or []:
557
+ for placeholder, val in replacements.items():
558
+ tok = tok.replace(placeholder, val)
559
+ argv.append(tok)
560
+
561
+ return {
562
+ "command": command,
563
+ "argv": argv,
564
+ "scope": cmd.get("scope"),
565
+ "inputs": cmd.get("inputs"),
566
+ "coverage": cmd.get("coverage"),
567
+ "source": cmd.get("source"),
568
+ }
569
+
570
+
571
+ # ---------------------------------------------------------------------------
572
+ # migrate(v1 → v2,dry-run/apply/备份)
573
+ # ---------------------------------------------------------------------------
574
+
575
+ def _backup_path(profile_path: Path) -> Path:
576
+ return profile_path.with_suffix(profile_path.suffix + ".v1.bak")
577
+
578
+
579
+ def migrate(project: Path, *, dry_run: bool = True) -> dict[str, Any]:
580
+ project = project.resolve()
581
+ profile_path = project / PROFILE_REL
582
+
583
+ if not profile_path.is_file():
584
+ return {
585
+ "ok": True,
586
+ "action": "migrate",
587
+ "dry_run": dry_run,
588
+ "needsMigration": False,
589
+ "reason": "no profile to migrate",
590
+ }
591
+
592
+ try:
593
+ existing = read_json(profile_path)
594
+ except (OSError, json.JSONDecodeError) as exc:
595
+ return {
596
+ "ok": False,
597
+ "action": "migrate",
598
+ "dry_run": dry_run,
599
+ "needsMigration": False,
600
+ "error": f"profile unreadable: {exc}",
601
+ }
602
+ if not isinstance(existing, dict):
603
+ return {
604
+ "ok": False,
605
+ "action": "migrate",
606
+ "dry_run": dry_run,
607
+ "needsMigration": False,
608
+ "error": "profile is not an object",
609
+ }
610
+
611
+ sv = existing.get("schemaVersion")
612
+ if sv == SCHEMA_VERSION:
613
+ return {
614
+ "ok": True,
615
+ "action": "migrate",
616
+ "dry_run": dry_run,
617
+ "needsMigration": False,
618
+ "reason": f"already schemaVersion {SCHEMA_VERSION}",
619
+ }
620
+ if sv != 1:
621
+ return {
622
+ "ok": False,
623
+ "action": "migrate",
624
+ "dry_run": dry_run,
625
+ "needsMigration": False,
626
+ "error": f"unsupported schemaVersion: {sv}",
627
+ }
628
+
629
+ # v1 → v2 迁移清单
630
+ changes = [
631
+ f"schemaVersion 1 → {SCHEMA_VERSION}",
632
+ "buildCommands → commands(含 argvTemplate/scope/inputs/coverage/source/basis)",
633
+ "excludedRoots 显式声明(.git/.harness/worktrees/target/build/node_modules/...)",
634
+ "identifier 约束(pattern/maxLength/prefix)",
635
+ ]
636
+ dropped: list[str] = []
637
+ v1_svc = existing.get("serviceStart") or {}
638
+ if isinstance(v1_svc, dict):
639
+ # 具体 worktree/change 路径必须清除(不属于持久 profile 模板)
640
+ for field in ("profile", "overlayPath"):
641
+ val = v1_svc.get(field, "")
642
+ if isinstance(val, str) and val.strip():
643
+ dropped.append(f"serviceStart.{field}={val!r}(具体路径,已清除)")
644
+ # 用户配置的服务命令/healthUrl 无 provenance,记录待确认(COM-007)
645
+ for field in ("command", "healthUrl"):
646
+ val = v1_svc.get(field, "")
647
+ if isinstance(val, str) and val.strip():
648
+ dropped.append(f"serviceStart.{field}(无 provenance,已备份;请用 record-quirk 重配)")
649
+
650
+ if dry_run:
651
+ return {
652
+ "ok": True,
653
+ "action": "migrate",
654
+ "dry_run": True,
655
+ "needsMigration": True,
656
+ "changes": changes,
657
+ "droppedFields": dropped,
658
+ "profilePath": str(profile_path),
659
+ }
660
+
661
+ # apply:先备份原 v1(不覆盖已有备份),再重新 detect 生成 v2
662
+ backup = _backup_path(profile_path)
663
+ if not backup.is_file():
664
+ backup.write_text(profile_path.read_text(encoding="utf-8-sig"), encoding="utf-8", newline="\n")
665
+ detect(project)
666
+ return {
667
+ "ok": True,
668
+ "action": "migrate",
669
+ "dry_run": False,
670
+ "needsMigration": False,
671
+ "changes": changes,
672
+ "droppedFields": dropped,
673
+ "backupPath": str(backup),
674
+ "profilePath": str(profile_path),
675
+ }
676
+
677
+
678
+ # ---------------------------------------------------------------------------
679
+ # CLI
680
+ # ---------------------------------------------------------------------------
681
+
682
+ def build_parser() -> argparse.ArgumentParser:
683
+ parser = argparse.ArgumentParser(
684
+ prog="harness_profile.py",
685
+ description="Harness profile v2: detect/check/validate/resolve/migrate",
686
+ )
687
+ parser.add_argument("--json", action="store_true", help="emit JSON")
688
+ sub = parser.add_subparsers(dest="command", required=True)
689
+
690
+ p_detect = sub.add_parser("detect", help="probe and write build-profile.json")
691
+ p_detect.add_argument("--project", required=True, type=Path)
692
+ p_detect.add_argument("--json", action="store_true")
693
+
694
+ p_check = sub.add_parser("check", help="missing/invalid/stale/ready")
695
+ p_check.add_argument("--project", required=True, type=Path)
696
+ p_check.add_argument("--json", action="store_true")
697
+
698
+ p_validate = sub.add_parser("validate", help="validate profile containment")
699
+ p_validate.add_argument("--project", required=True, type=Path)
700
+ p_validate.add_argument("--json", action="store_true")
701
+
702
+ p_migrate = sub.add_parser("migrate", help="migrate v1 → v2")
703
+ p_migrate.add_argument("--project", required=True, type=Path)
704
+ p_migrate.add_argument("--apply", action="store_true", help="apply (default: dry-run)")
705
+ p_migrate.add_argument("--json", action="store_true")
706
+
707
+ return parser
708
+
709
+
710
+ def main(argv: list[str] | None = None) -> int:
711
+ parser = build_parser()
712
+ args = parser.parse_args(argv)
713
+ as_json = bool(getattr(args, "json", False))
714
+
715
+ if args.command == "detect":
716
+ result = detect(args.project)
717
+ sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
718
+ return 0 if result.get("ok") else 1
719
+ if args.command == "check":
720
+ result = check(args.project)
721
+ sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
722
+ return 0 if not result.get("stale") else 0 # stale still exits 0 (callers branch on JSON)
723
+ if args.command == "validate":
724
+ profile = load_profile(args.project.resolve())
725
+ if profile is None:
726
+ payload = {"ok": False, "issues": ["profile missing"]}
727
+ else:
728
+ issues = validate_profile(profile, args.project.resolve())
729
+ payload = {"ok": not issues, "issues": issues}
730
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
731
+ return 0 if payload["ok"] else 1
732
+ if args.command == "migrate":
733
+ result = migrate(args.project, dry_run=not args.apply)
734
+ sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
735
+ return 0 if result.get("ok") else 1
736
+
737
+ parser.error(f"unknown command: {args.command}")
738
+ return 2
739
+
740
+
741
+ if __name__ == "__main__":
742
+ raise SystemExit(main())