@hunter-harness/workflow-harness 0.2.39 → 0.2.41

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 (230) 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/checklist.md +3 -0
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  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 +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
  16. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
  17. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
  18. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
  19. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
  20. package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
  22. package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
  23. package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
  26. package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
  27. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
  28. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  29. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
  31. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  32. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
  37. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
  43. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
  44. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
  45. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
  46. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
  47. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
  48. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
  49. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
  50. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
  51. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
  52. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
  53. package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
  54. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
  55. package/harness/bundles/general/codex/.harness-build.json +1 -1
  56. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
  58. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
  59. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  62. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
  64. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  67. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
  70. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
  71. package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
  72. package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
  73. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
  74. package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
  75. package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
  76. package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
  77. package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
  78. package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
  79. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
  80. package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
  81. package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
  82. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  83. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
  85. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
  86. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  87. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  90. package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
  91. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  94. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
  97. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
  98. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
  99. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
  100. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
  101. package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
  102. package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
  103. package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
  104. package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
  105. package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
  106. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
  107. package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
  108. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
  109. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  110. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
  113. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  114. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  115. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  118. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
  120. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
  125. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
  126. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
  127. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
  128. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
  129. package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
  130. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
  131. package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
  132. package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
  133. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
  134. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
  135. package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
  136. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
  137. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  138. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
  141. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  142. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  144. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
  148. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
  153. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
  154. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
  155. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
  156. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
  157. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
  158. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
  159. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
  160. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
  161. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
  162. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
  163. package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
  164. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
  165. package/harness/bundles/java/codex/.harness-build.json +1 -1
  166. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  168. package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
  169. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
  170. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  175. package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
  176. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
  181. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
  182. package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
  183. package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
  184. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
  185. package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
  186. package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
  187. package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
  188. package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
  189. package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
  190. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
  191. package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
  192. package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
  193. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  194. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  195. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  196. package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
  197. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
  198. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  199. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  200. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  201. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
  204. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  206. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  207. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
  209. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
  210. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
  211. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
  212. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
  213. package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
  214. package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
  215. package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
  216. package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
  217. package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
  218. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
  219. package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
  220. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
  221. package/harness/manifests/general/claude-code.json +29 -29
  222. package/harness/manifests/general/codebuddy.json +29 -29
  223. package/harness/manifests/general/codex.json +29 -29
  224. package/harness/manifests/general/cursor.json +29 -29
  225. package/harness/manifests/java/claude-code.json +30 -30
  226. package/harness/manifests/java/codebuddy.json +30 -30
  227. package/harness/manifests/java/codex.json +30 -30
  228. package/harness/manifests/java/cursor.json +30 -30
  229. package/hunter-workflow-family.json +4 -4
  230. package/package.json +1 -1
@@ -888,6 +888,181 @@ def ownership_hash(contract: dict[str, Any]) -> str:
888
888
  return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
889
889
 
890
890
 
891
+ class LedgerMigrationError(ValueError):
892
+ """A legacy ledger could not be upgraded without inventing identity."""
893
+
894
+
895
+ def ledger_evidence_identity(ledger: dict[str, Any]) -> str:
896
+ """Stable identity of evidence-bearing fields, excluding schema metadata."""
897
+ evidence = {
898
+ key: ledger[key]
899
+ for key in (
900
+ "validations",
901
+ "verificationTargets",
902
+ "integration",
903
+ "artifacts",
904
+ )
905
+ if key in ledger
906
+ }
907
+ canonical = json.dumps(
908
+ evidence,
909
+ ensure_ascii=False,
910
+ sort_keys=True,
911
+ separators=(",", ":"),
912
+ ).encode("utf-8")
913
+ return "sha256:" + hashlib.sha256(canonical).hexdigest()
914
+
915
+
916
+ def migrate_ledger_for_write(
917
+ change_dir: Path,
918
+ ledger: dict[str, Any],
919
+ *,
920
+ project_root: Path | None,
921
+ base_commit: str | None,
922
+ diff_hash: str | None,
923
+ record_migration: bool,
924
+ ) -> tuple[dict[str, Any], dict[str, Any] | None]:
925
+ """Upgrade one supported legacy window to v3 before the caller mutates evidence."""
926
+ raw_schema = ledger.get("schemaVersion")
927
+ source_schema = raw_schema if isinstance(raw_schema, int) else 1
928
+ if source_schema not in {1, 2, LEDGER_SCHEMA_VERSION}:
929
+ raise LedgerMigrationError(
930
+ f"unsupported ledger schemaVersion={raw_schema!r}; "
931
+ f"supported source versions are 1, 2, and {LEDGER_SCHEMA_VERSION}"
932
+ )
933
+ repo_probe = project_root or change_dir
934
+ repo_root_raw = _git_text(repo_probe, "rev-parse", "--show-toplevel")
935
+ current_head = _git_text(repo_probe, "rev-parse", "--verify", "HEAD")
936
+ if not repo_root_raw or not current_head:
937
+ raise LedgerMigrationError(
938
+ "repository root/current HEAD could not be established"
939
+ )
940
+ repo_root = Path(repo_root_raw).resolve()
941
+ try:
942
+ contract = harness_paths.load_change_contract(change_dir)
943
+ except (OSError, ValueError) as exc:
944
+ raise LedgerMigrationError(
945
+ f"change ownership contract could not be loaded: {exc}"
946
+ ) from exc
947
+
948
+ resolved_base = base_commit
949
+ if not _nonempty_str(resolved_base):
950
+ resolved_base = ledger.get("baseCommit")
951
+ if not _nonempty_str(resolved_base):
952
+ resolved_base = current_head
953
+ resolved_diff = diff_hash
954
+ if not _nonempty_str(resolved_diff):
955
+ try:
956
+ resolved_diff = compute_ownership_diff(
957
+ repo_root,
958
+ base=str(resolved_base).strip(),
959
+ change_dir=change_dir,
960
+ )["diffHash"]
961
+ except (OSError, ValueError, RuntimeError) as exc:
962
+ raise LedgerMigrationError(
963
+ f"ownership diff identity could not be computed: {exc}"
964
+ ) from exc
965
+
966
+ before_identity = ledger_evidence_identity(ledger)
967
+ migrated = dict(ledger)
968
+ migrated["schemaVersion"] = LEDGER_SCHEMA_VERSION
969
+ try:
970
+ migrated["repositoryId"] = harness_paths.repository_identity(repo_root)
971
+ migrated["ownershipHash"] = ownership_hash(contract)
972
+ except (OSError, TypeError, ValueError, RuntimeError) as exc:
973
+ raise LedgerMigrationError(
974
+ f"repository/ownership identity could not be established: {exc}"
975
+ ) from exc
976
+ migrated["changeName"] = change_dir.name
977
+ migrated["baseCommit"] = str(resolved_base).strip()
978
+ migrated["currentHead"] = str(current_head).strip()
979
+ migrated["diffHash"] = str(resolved_diff).strip()
980
+ missing = validate_ledger_identity(migrated)
981
+ if missing:
982
+ raise LedgerMigrationError(
983
+ "migrated ledger identity is incomplete: " + ", ".join(missing)
984
+ )
985
+
986
+ receipt: dict[str, Any] | None = None
987
+ if source_schema != LEDGER_SCHEMA_VERSION and record_migration:
988
+ after_identity = ledger_evidence_identity(migrated)
989
+ if after_identity != before_identity:
990
+ raise LedgerMigrationError(
991
+ "migration changed pre-existing evidence identity"
992
+ )
993
+ receipt = {
994
+ "schemaVersion": 1,
995
+ "action": "ledger-schema-migration",
996
+ "originalSchemaVersion": source_schema,
997
+ "targetSchemaVersion": LEDGER_SCHEMA_VERSION,
998
+ "migratedAt": now_iso(),
999
+ "evidenceIdentityBefore": before_identity,
1000
+ "evidenceIdentityAfter": after_identity,
1001
+ "repositoryId": migrated["repositoryId"],
1002
+ "baseCommit": migrated["baseCommit"],
1003
+ "currentHead": migrated["currentHead"],
1004
+ "diffHash": migrated["diffHash"],
1005
+ }
1006
+ receipt_identity = json.dumps(
1007
+ receipt,
1008
+ ensure_ascii=False,
1009
+ sort_keys=True,
1010
+ separators=(",", ":"),
1011
+ ).encode("utf-8")
1012
+ receipt["receiptId"] = (
1013
+ "sha256:" + hashlib.sha256(receipt_identity).hexdigest()
1014
+ )
1015
+ history = migrated.get("migrationHistory")
1016
+ if history is None:
1017
+ history = []
1018
+ if not isinstance(history, list):
1019
+ raise LedgerMigrationError("migrationHistory must be an array")
1020
+ migrated["migrationHistory"] = [*history, receipt]
1021
+ return migrated, receipt
1022
+
1023
+
1024
+ def _deterministic_rerecord_command(
1025
+ args: argparse.Namespace,
1026
+ *,
1027
+ files: list[str],
1028
+ project_root: Path | None,
1029
+ ) -> list[str]:
1030
+ command = [
1031
+ sys.executable,
1032
+ str(Path(__file__).resolve()),
1033
+ "--json",
1034
+ "record",
1035
+ "--change-dir",
1036
+ str(resolve_path(args.change_dir)),
1037
+ "--verification",
1038
+ str(args.verification),
1039
+ "--status",
1040
+ str(args.status),
1041
+ "--command",
1042
+ str(args.command),
1043
+ "--exit-code",
1044
+ str(args.exit_code),
1045
+ "--duration-ms",
1046
+ str(args.duration_ms),
1047
+ "--evidence",
1048
+ str(args.evidence),
1049
+ "--files",
1050
+ ",".join(sorted(str(Path(item).resolve()) for item in files)),
1051
+ ]
1052
+ if project_root is not None:
1053
+ command.extend(["--project", str(project_root)])
1054
+ for flag, attr in (
1055
+ ("--scope", "scope"),
1056
+ ("--coverage", "coverage"),
1057
+ ("--base-commit", "base_commit"),
1058
+ ("--diff-hash", "diff_hash"),
1059
+ ):
1060
+ value = getattr(args, attr, None)
1061
+ if _nonempty_str(value):
1062
+ command.extend([flag, str(value).strip()])
1063
+ return command
1064
+
1065
+
891
1066
  _METRICS_SCHEMAS: dict[str, dict[str, tuple[str, ...]]] = {
892
1067
  "unitTest": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
893
1068
  "unitTestFull": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
@@ -1125,6 +1300,266 @@ def _nonempty_str(value: Any) -> bool:
1125
1300
  return isinstance(value, str) and bool(value.strip())
1126
1301
 
1127
1302
 
1303
+ def _scenario_receipt_error(code: str, message: str) -> dict[str, Any]:
1304
+ return {"ok": False, "code": code, "error": message}
1305
+
1306
+
1307
+ def _canonical_string_list(
1308
+ receipt: dict[str, Any],
1309
+ field: str,
1310
+ ) -> tuple[list[str] | None, dict[str, Any] | None]:
1311
+ raw = receipt.get(field)
1312
+ if not isinstance(raw, list) or any(not _nonempty_str(item) for item in raw):
1313
+ return None, _scenario_receipt_error(
1314
+ "SCENARIO_RECEIPT_INVALID",
1315
+ f"scenario receipt field '{field}' must be an array of non-empty strings",
1316
+ )
1317
+ values = [str(item).strip() for item in raw]
1318
+ if len(values) != len(set(values)):
1319
+ return None, _scenario_receipt_error(
1320
+ "SCENARIO_RECEIPT_INVALID",
1321
+ f"scenario receipt field '{field}' contains duplicate test IDs",
1322
+ )
1323
+ return values, None
1324
+
1325
+
1326
+ def validate_scenario_execution_receipt(
1327
+ *,
1328
+ change_dir: Path,
1329
+ scenario_ids: list[str],
1330
+ receipt: dict[str, Any],
1331
+ ) -> dict[str, Any]:
1332
+ """Validate and minimize runner output before it can satisfy scenario evidence."""
1333
+ manifest_path = change_dir / "meta" / "scenario-manifest.json"
1334
+ try:
1335
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
1336
+ except FileNotFoundError:
1337
+ return _scenario_receipt_error(
1338
+ "SCENARIO_MANIFEST_MISSING",
1339
+ f"scenario manifest does not exist: {manifest_path}",
1340
+ )
1341
+ except (OSError, json.JSONDecodeError) as exc:
1342
+ return _scenario_receipt_error("SCENARIO_MANIFEST_INVALID", str(exc))
1343
+ if not isinstance(manifest, dict) or not isinstance(manifest.get("scenarios"), list):
1344
+ return _scenario_receipt_error(
1345
+ "SCENARIO_MANIFEST_INVALID",
1346
+ "scenario-manifest.json must contain a scenarios array",
1347
+ )
1348
+ if int(manifest.get("schemaVersion") or 0) < 2:
1349
+ return _scenario_receipt_error(
1350
+ "SCENARIO_RECEIPT_UNSUPPORTED",
1351
+ "structured scenario receipts require scenario-manifest schemaVersion 2",
1352
+ )
1353
+ if receipt.get("schemaVersion") != 1:
1354
+ return _scenario_receipt_error(
1355
+ "SCENARIO_RECEIPT_INVALID",
1356
+ "scenario receipt schemaVersion must be 1",
1357
+ )
1358
+ runner = receipt.get("runner")
1359
+ if not isinstance(runner, dict) or not _nonempty_str(runner.get("name")):
1360
+ return _scenario_receipt_error(
1361
+ "SCENARIO_RECEIPT_INVALID",
1362
+ "scenario receipt runner.name is required",
1363
+ )
1364
+ if "version" in runner and not _nonempty_str(runner.get("version")):
1365
+ return _scenario_receipt_error(
1366
+ "SCENARIO_RECEIPT_INVALID",
1367
+ "scenario receipt runner.version must be a non-empty string when present",
1368
+ )
1369
+ attempt = receipt.get("attempt")
1370
+ if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
1371
+ return _scenario_receipt_error(
1372
+ "SCENARIO_RECEIPT_INVALID",
1373
+ "scenario receipt attempt must be a positive integer",
1374
+ )
1375
+
1376
+ declared, error = _canonical_string_list(receipt, "declared")
1377
+ if error:
1378
+ return error
1379
+ selected, error = _canonical_string_list(receipt, "selected")
1380
+ if error:
1381
+ return error
1382
+ assert declared is not None and selected is not None
1383
+
1384
+ collected_raw = receipt.get("collected")
1385
+ executed_raw = receipt.get("executed")
1386
+ if not isinstance(collected_raw, list) or not isinstance(executed_raw, list):
1387
+ return _scenario_receipt_error(
1388
+ "SCENARIO_RECEIPT_INVALID",
1389
+ "scenario receipt collected and executed fields must be arrays",
1390
+ )
1391
+
1392
+ collected: list[dict[str, str]] = []
1393
+ collected_keys: set[tuple[str, str, str]] = set()
1394
+ for index, item in enumerate(collected_raw):
1395
+ if not isinstance(item, dict) or any(
1396
+ not _nonempty_str(item.get(field))
1397
+ for field in ("testId", "file", "title")
1398
+ ):
1399
+ return _scenario_receipt_error(
1400
+ "SCENARIO_RECEIPT_INVALID",
1401
+ f"scenario receipt collected[{index}] has an invalid test identity",
1402
+ )
1403
+ canonical = {
1404
+ "testId": str(item["testId"]).strip(),
1405
+ "file": str(item["file"]).strip(),
1406
+ "title": str(item["title"]).strip(),
1407
+ }
1408
+ key = (canonical["testId"], canonical["file"], canonical["title"])
1409
+ if key in collected_keys:
1410
+ return _scenario_receipt_error(
1411
+ "SCENARIO_RECEIPT_INVALID",
1412
+ f"scenario receipt collected[{index}] duplicates a test identity",
1413
+ )
1414
+ collected_keys.add(key)
1415
+ collected.append(canonical)
1416
+
1417
+ executed: list[dict[str, Any]] = []
1418
+ executed_keys: set[tuple[str, str, str, int]] = set()
1419
+ allowed_statuses = {"PASSED", "FAILED", "SKIPPED"}
1420
+ for index, item in enumerate(executed_raw):
1421
+ if not isinstance(item, dict) or any(
1422
+ not _nonempty_str(item.get(field))
1423
+ for field in ("testId", "file", "title", "status")
1424
+ ):
1425
+ return _scenario_receipt_error(
1426
+ "SCENARIO_RECEIPT_INVALID",
1427
+ f"scenario receipt executed[{index}] has an invalid test identity",
1428
+ )
1429
+ item_attempt = item.get("attempt")
1430
+ if (
1431
+ not isinstance(item_attempt, int)
1432
+ or isinstance(item_attempt, bool)
1433
+ or item_attempt != attempt
1434
+ ):
1435
+ return _scenario_receipt_error(
1436
+ "SCENARIO_RECEIPT_INVALID",
1437
+ f"scenario receipt executed[{index}].attempt must equal receipt attempt",
1438
+ )
1439
+ item_status = str(item["status"]).strip().upper()
1440
+ if item_status not in allowed_statuses:
1441
+ return _scenario_receipt_error(
1442
+ "SCENARIO_RECEIPT_INVALID",
1443
+ f"scenario receipt executed[{index}].status is unsupported",
1444
+ )
1445
+ canonical = {
1446
+ "testId": str(item["testId"]).strip(),
1447
+ "file": str(item["file"]).strip(),
1448
+ "title": str(item["title"]).strip(),
1449
+ "attempt": item_attempt,
1450
+ "status": item_status,
1451
+ }
1452
+ key = (
1453
+ canonical["testId"],
1454
+ canonical["file"],
1455
+ canonical["title"],
1456
+ item_attempt,
1457
+ )
1458
+ if key in executed_keys:
1459
+ return _scenario_receipt_error(
1460
+ "SCENARIO_RECEIPT_INVALID",
1461
+ f"scenario receipt executed[{index}] duplicates a test attempt",
1462
+ )
1463
+ executed_keys.add(key)
1464
+ executed.append(canonical)
1465
+
1466
+ manifest_scenarios = {
1467
+ str(item.get("id") or "").strip(): item
1468
+ for item in manifest["scenarios"]
1469
+ if isinstance(item, dict) and _nonempty_str(item.get("id"))
1470
+ }
1471
+ unknown = sorted(set(scenario_ids) - set(manifest_scenarios))
1472
+ if unknown:
1473
+ return _scenario_receipt_error(
1474
+ "SCENARIO_ID_UNKNOWN",
1475
+ "scenario IDs are not declared in scenario-manifest.json: "
1476
+ + ", ".join(unknown),
1477
+ )
1478
+
1479
+ coverage = {
1480
+ key: []
1481
+ for key in (
1482
+ "declared",
1483
+ "selected",
1484
+ "collected",
1485
+ "executed",
1486
+ "passed",
1487
+ "skipped",
1488
+ "failed",
1489
+ "unexecuted",
1490
+ )
1491
+ }
1492
+ for scenario_id in scenario_ids:
1493
+ scenario = manifest_scenarios[scenario_id]
1494
+ if scenario.get("requiredEvidenceKind") != "ledger":
1495
+ continue
1496
+ if any(
1497
+ not _nonempty_str(scenario.get(field))
1498
+ for field in ("executableTestId", "testFile", "testTitle")
1499
+ ):
1500
+ return _scenario_receipt_error(
1501
+ "SCENARIO_MANIFEST_INVALID",
1502
+ f"scenario {scenario_id} is missing its executable test identity",
1503
+ )
1504
+ test_id = str(scenario["executableTestId"]).strip()
1505
+ identity = (
1506
+ test_id,
1507
+ str(scenario["testFile"]).strip(),
1508
+ str(scenario["testTitle"]).strip(),
1509
+ )
1510
+ if test_id in declared:
1511
+ coverage["declared"].append(scenario_id)
1512
+ if test_id in selected:
1513
+ coverage["selected"].append(scenario_id)
1514
+ if identity in collected_keys:
1515
+ coverage["collected"].append(scenario_id)
1516
+ selection_closed = (
1517
+ test_id in declared
1518
+ and test_id in selected
1519
+ and identity in collected_keys
1520
+ )
1521
+ matches = [
1522
+ item
1523
+ for item in executed
1524
+ if (
1525
+ item["testId"],
1526
+ item["file"],
1527
+ item["title"],
1528
+ )
1529
+ == identity
1530
+ ]
1531
+ if matches:
1532
+ coverage["executed"].append(scenario_id)
1533
+ terminal = matches[-1]["status"]
1534
+ if terminal == "PASSED" and selection_closed:
1535
+ coverage["passed"].append(scenario_id)
1536
+ elif terminal == "SKIPPED":
1537
+ coverage["skipped"].append(scenario_id)
1538
+ elif terminal == "FAILED":
1539
+ coverage["failed"].append(scenario_id)
1540
+ if not selection_closed:
1541
+ coverage["unexecuted"].append(scenario_id)
1542
+ else:
1543
+ coverage["unexecuted"].append(scenario_id)
1544
+
1545
+ canonical_runner = {"name": str(runner["name"]).strip()}
1546
+ if _nonempty_str(runner.get("version")):
1547
+ canonical_runner["version"] = str(runner["version"]).strip()
1548
+ return {
1549
+ "ok": True,
1550
+ "receipt": {
1551
+ "schemaVersion": 1,
1552
+ "runner": canonical_runner,
1553
+ "attempt": attempt,
1554
+ "declared": declared,
1555
+ "selected": selected,
1556
+ "collected": collected,
1557
+ "executed": executed,
1558
+ },
1559
+ "coverage": coverage,
1560
+ }
1561
+
1562
+
1128
1563
  def _scope_covers(ledger_scope: Any, requested_scope: str | None) -> bool:
1129
1564
  """unitTest: ledger scope must cover requested scope (broad scopes cover all)."""
1130
1565
  if not _nonempty_str(ledger_scope) and not isinstance(ledger_scope, list):
@@ -1702,14 +2137,7 @@ def cmd_record(args: argparse.Namespace) -> int:
1702
2137
  change_dir = resolve_path(args.change_dir)
1703
2138
  verification = args.verification
1704
2139
  contract_v2 = _contract_is_v2(change_dir)
1705
- if not contract_v2 and any(
1706
- _nonempty_str(getattr(args, field, None))
1707
- for field in ("base_commit", "diff_hash")
1708
- ):
1709
- return emit_error(
1710
- "IDENTITY_UNSUPPORTED: explicit ledger identity requires a v2 change contract",
1711
- as_json=as_json,
1712
- )
2140
+ migration_receipt: dict[str, Any] | None = None
1713
2141
  try:
1714
2142
  files, project_root = input_files_from_args(args)
1715
2143
  except (OSError, ValueError) as exc:
@@ -1742,7 +2170,7 @@ def cmd_record(args: argparse.Namespace) -> int:
1742
2170
  inputs_hash, inputs_files = compute_inputs_hash(
1743
2171
  files, project_root=project_root
1744
2172
  )
1745
- ledger, _existing_path = load_ledger(change_dir)
2173
+ ledger, existing_path = load_ledger(change_dir)
1746
2174
  if ledger is None:
1747
2175
  ledger = {
1748
2176
  "changeName": change_dir.name,
@@ -1751,6 +2179,41 @@ def cmd_record(args: argparse.Namespace) -> int:
1751
2179
  }
1752
2180
  elif not isinstance(ledger.get("validations"), dict):
1753
2181
  ledger["validations"] = {}
2182
+ explicit_identity = any(
2183
+ _nonempty_str(getattr(args, field, None))
2184
+ for field in ("base_commit", "diff_hash")
2185
+ )
2186
+ existing_schema = ledger.get("schemaVersion")
2187
+ identity_write = (
2188
+ contract_v2
2189
+ or explicit_identity
2190
+ or existing_schema in {2, LEDGER_SCHEMA_VERSION}
2191
+ )
2192
+ if identity_write:
2193
+ try:
2194
+ ledger, migration_receipt = migrate_ledger_for_write(
2195
+ change_dir,
2196
+ ledger,
2197
+ project_root=project_root,
2198
+ base_commit=getattr(args, "base_commit", None),
2199
+ diff_hash=getattr(args, "diff_hash", None),
2200
+ record_migration=existing_path is not None,
2201
+ )
2202
+ except LedgerMigrationError as exc:
2203
+ return emit_error(
2204
+ f"ledger migration failed: {exc}",
2205
+ as_json=as_json,
2206
+ error_code="LEDGER_MIGRATION_REQUIRED",
2207
+ extra={
2208
+ "originalSchemaVersion": ledger.get("schemaVersion", 1),
2209
+ "targetSchemaVersion": LEDGER_SCHEMA_VERSION,
2210
+ "rerecordCommand": _deterministic_rerecord_command(
2211
+ args,
2212
+ files=files,
2213
+ project_root=project_root,
2214
+ ),
2215
+ },
2216
+ )
1754
2217
 
1755
2218
  # Preserve top-level diffHash and all other existing fields (backward compatible).
1756
2219
  entry = {}
@@ -1857,13 +2320,115 @@ def cmd_record(args: argparse.Namespace) -> int:
1857
2320
 
1858
2321
  # C9: bind scenario IDs from --scenario-ids to this ledger entry.
1859
2322
  scenario_ids_raw = getattr(args, "scenario_ids", None)
2323
+ scenario_receipt_file = getattr(args, "scenario_receipt_file", None)
2324
+ if _nonempty_str(scenario_receipt_file) and not _nonempty_str(
2325
+ scenario_ids_raw
2326
+ ):
2327
+ return emit_error(
2328
+ "--scenario-receipt-file requires --scenario-ids",
2329
+ as_json=as_json,
2330
+ error_code="SCENARIO_IDS_REQUIRED",
2331
+ )
1860
2332
  if _nonempty_str(scenario_ids_raw):
1861
2333
  ids = [s.strip() for s in str(scenario_ids_raw).split(",") if s.strip()]
2334
+ if len(ids) != len(set(ids)):
2335
+ return emit_error(
2336
+ "scenario IDs must be unique",
2337
+ as_json=as_json,
2338
+ error_code="SCENARIO_ID_DUPLICATE",
2339
+ )
1862
2340
  if ids:
1863
2341
  entry["scenarioIds"] = ids
2342
+ receipt_file = scenario_receipt_file
2343
+ manifest_path = change_dir / "meta" / "scenario-manifest.json"
2344
+ manifest_schema = 0
2345
+ if manifest_path.is_file():
2346
+ try:
2347
+ manifest_probe = json.loads(
2348
+ manifest_path.read_text(encoding="utf-8-sig")
2349
+ )
2350
+ except (OSError, json.JSONDecodeError) as exc:
2351
+ return emit_error(
2352
+ f"invalid scenario manifest: {exc}",
2353
+ as_json=as_json,
2354
+ error_code="SCENARIO_MANIFEST_INVALID",
2355
+ )
2356
+ raw_schema = (
2357
+ manifest_probe.get("schemaVersion")
2358
+ if isinstance(manifest_probe, dict)
2359
+ else None
2360
+ )
2361
+ if (
2362
+ isinstance(raw_schema, int)
2363
+ and not isinstance(raw_schema, bool)
2364
+ ):
2365
+ manifest_schema = raw_schema
2366
+ if manifest_schema >= 2 and not _nonempty_str(receipt_file):
2367
+ return emit_error(
2368
+ "scenario-manifest schemaVersion 2 requires "
2369
+ "--scenario-receipt-file",
2370
+ as_json=as_json,
2371
+ error_code="SCENARIO_RECEIPT_REQUIRED",
2372
+ )
2373
+ if _nonempty_str(receipt_file):
2374
+ try:
2375
+ receipt_payload = json.loads(
2376
+ Path(str(receipt_file))
2377
+ .expanduser()
2378
+ .resolve()
2379
+ .read_text(encoding="utf-8-sig")
2380
+ )
2381
+ except (OSError, json.JSONDecodeError) as exc:
2382
+ return emit_error(
2383
+ f"invalid scenario receipt: {exc}",
2384
+ as_json=as_json,
2385
+ error_code="SCENARIO_RECEIPT_INVALID",
2386
+ )
2387
+ if not isinstance(receipt_payload, dict):
2388
+ return emit_error(
2389
+ "scenario receipt top level must be an object",
2390
+ as_json=as_json,
2391
+ error_code="SCENARIO_RECEIPT_INVALID",
2392
+ )
2393
+ scenario_result = validate_scenario_execution_receipt(
2394
+ change_dir=change_dir,
2395
+ scenario_ids=ids,
2396
+ receipt=receipt_payload,
2397
+ )
2398
+ if not scenario_result.get("ok"):
2399
+ return emit_error(
2400
+ str(scenario_result.get("error") or "invalid scenario receipt"),
2401
+ as_json=as_json,
2402
+ error_code=str(
2403
+ scenario_result.get("code")
2404
+ or "SCENARIO_RECEIPT_INVALID"
2405
+ ),
2406
+ )
2407
+ entry["scenarioReceipt"] = scenario_result["receipt"]
2408
+ entry["scenarioCoverage"] = scenario_result["coverage"]
2409
+ if status == "OK":
2410
+ passed = set(scenario_result["coverage"]["passed"])
2411
+ required = set(ids)
2412
+ missing = sorted(required - passed)
2413
+ if missing:
2414
+ return emit_error(
2415
+ "required scenarios were not executed successfully: "
2416
+ + ", ".join(missing),
2417
+ as_json=as_json,
2418
+ error_code="REQUIRED_SCENARIO_NOT_EXECUTED",
2419
+ extra={
2420
+ "missing": missing,
2421
+ "scenarioCoverage": scenario_result["coverage"],
2422
+ },
2423
+ )
2424
+ else:
2425
+ entry.pop("scenarioReceipt", None)
2426
+ entry.pop("scenarioCoverage", None)
1864
2427
  elif "scenarioIds" in entry:
1865
2428
  # Fresh record without scenario-ids must not keep stale ids from prev.
1866
2429
  entry.pop("scenarioIds", None)
2430
+ entry.pop("scenarioReceipt", None)
2431
+ entry.pop("scenarioCoverage", None)
1867
2432
 
1868
2433
  target_identity = {
1869
2434
  "verification": verification,
@@ -1908,52 +2473,6 @@ def cmd_record(args: argparse.Namespace) -> int:
1908
2473
  ledger["stateDir"] = str(change_dir)
1909
2474
 
1910
2475
  out_path = preferred_write_path(change_dir)
1911
- if contract_v2:
1912
- # Ledger v3: forced top-level identity (RET-15/16, COM-002).
1913
- repo_probe = project_root or change_dir
1914
- repo_root_raw = _git_text(repo_probe, "rev-parse", "--show-toplevel")
1915
- if not repo_root_raw:
1916
- return emit_error(
1917
- "record failed: change dir is not inside a git repository",
1918
- as_json=as_json,
1919
- )
1920
- repo_root = Path(repo_root_raw).resolve()
1921
- try:
1922
- contract = harness_paths.load_change_contract(change_dir)
1923
- except (OSError, ValueError) as exc:
1924
- return emit_error(f"record failed: {exc}", as_json=as_json)
1925
- base_commit = getattr(args, "base_commit", None)
1926
- if not _nonempty_str(base_commit):
1927
- base_commit = ledger.get("baseCommit")
1928
- if not _nonempty_str(base_commit):
1929
- base_commit = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1930
- current_head = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1931
- diff_hash = getattr(args, "diff_hash", None)
1932
- if not _nonempty_str(diff_hash) and _nonempty_str(base_commit):
1933
- try:
1934
- diff_hash = compute_ownership_diff(
1935
- repo_root, base=str(base_commit).strip(), change_dir=change_dir
1936
- )["diffHash"]
1937
- except (OSError, ValueError, RuntimeError):
1938
- diff_hash = None
1939
- ledger["schemaVersion"] = LEDGER_SCHEMA_VERSION
1940
- ledger["repositoryId"] = harness_paths.repository_identity(repo_root)
1941
- ledger["changeName"] = change_dir.name
1942
- if _nonempty_str(base_commit):
1943
- ledger["baseCommit"] = str(base_commit).strip()
1944
- if _nonempty_str(current_head):
1945
- ledger["currentHead"] = str(current_head).strip()
1946
- if _nonempty_str(diff_hash):
1947
- ledger["diffHash"] = str(diff_hash).strip()
1948
- ledger["ownershipHash"] = ownership_hash(contract)
1949
- missing = validate_ledger_identity(ledger)
1950
- if missing:
1951
- return emit_error(
1952
- "ledger identity incomplete: " + ", ".join(missing),
1953
- as_json=as_json,
1954
- error_code="LEDGER_IDENTITY_INVALID",
1955
- extra={"missing": missing},
1956
- )
1957
2476
  write_ledger(out_path, ledger)
1958
2477
  try:
1959
2478
  upsert_verification_graph_node(
@@ -2004,6 +2523,8 @@ def cmd_record(args: argparse.Namespace) -> int:
2004
2523
  "ledger_path": str(out_path),
2005
2524
  "diffHash": ledger.get("diffHash"),
2006
2525
  "resolvedProjectRoot": str(project_root) if project_root else None,
2526
+ "scenarioCoverage": entry.get("scenarioCoverage"),
2527
+ "migrationReceipt": migration_receipt,
2007
2528
  }
2008
2529
  emit_compact_or_verbose(
2009
2530
  payload, as_json=as_json, verbose=verbose,
@@ -2209,6 +2730,11 @@ def build_parser() -> argparse.ArgumentParser:
2209
2730
  default=None,
2210
2731
  help="comma-separated scenario IDs from scenario-manifest.json to bind to this entry",
2211
2732
  )
2733
+ p_record.add_argument(
2734
+ "--scenario-receipt-file",
2735
+ default=None,
2736
+ help="UTF-8 JSON runner receipt proving declared/selected/collected/executed tests",
2737
+ )
2212
2738
  p_record.add_argument(
2213
2739
  "--verbose",
2214
2740
  action="store_true",