ace-assign 0.71.0 → 0.73.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "open3"
5
- require_relative "../errors"
6
-
7
- module Ace
8
- module Assign
9
- module Molecules
10
- # Invokes the maintained ordinary fixed merge producer through the
11
- # installed `ace-lab service request/status --mode ordinary-merge`
12
- # CLI. The argv is closed: no shell, no caller-selected executable,
13
- # socket, evidence path or token selector beyond the published request
14
- # form. Output is bounded and parsed as one strict JSON envelope.
15
- #
16
- # Any local execution, framing or parse failure raises
17
- # UnavailableError: a caller-side blocker that never proves refusal
18
- # and never authorizes resubmission.
19
- class OrdinaryMergeCli
20
- MAX_OUTPUT_BYTES = 16 * 1024
21
-
22
- # Raised when no maintained producer response is obtainable before
23
- # the envelope could be observed. Maps to the closed
24
- # `ordinary_merge_unavailable` caller-side blocker.
25
- class UnavailableError < Ace::Assign::AttemptErrors::EvidenceUnavailable
26
- def initialize(message = "ordinary merge producer response unavailable")
27
- super
28
- end
29
- end
30
-
31
- def initialize(binary: "ace-lab")
32
- @binary = binary
33
- end
34
-
35
- def request(project:, assignment:, attempt:, queue_entry:, input_path:, authorization:, request_id:)
36
- run(["service", "request", "--mode", "ordinary-merge",
37
- "--project", project, "--assignment", assignment, "--attempt", attempt,
38
- "--queue-entry", queue_entry, "--operation", "merge", "--input", input_path,
39
- "--authorization", authorization, "--request-id", request_id])
40
- end
41
-
42
- def status(reference:, request_id:, project:, assignment:, attempt:, queue_entry:, input_digest:)
43
- run(["service", "status", "--mode", "ordinary-merge", "--reference", reference,
44
- "--request", request_id, "--project", project, "--assignment", assignment,
45
- "--attempt", attempt, "--queue-entry", queue_entry,
46
- "--input-digest", input_digest, "--format", "json"])
47
- end
48
-
49
- private
50
-
51
- def run(arguments)
52
- output, = Open3.capture3(@binary, *arguments)
53
- raise UnavailableError, "ordinary merge producer output is missing or exceeds its bound" unless
54
- output.is_a?(String) && !output.empty? && output.bytesize <= MAX_OUTPUT_BYTES
55
- envelope = JSON.parse(output, max_nesting: 16, create_additions: false,
56
- allow_duplicate_key: false, allow_comments: false, allow_nan: false)
57
- unless envelope.is_a?(Hash) && envelope["status"].is_a?(String) &&
58
- (envelope.keys - %w[status data error]).empty?
59
- raise UnavailableError, "ordinary merge producer output is not a maintained envelope"
60
- end
61
- envelope
62
- rescue JSON::ParserError, SystemCallError
63
- raise UnavailableError, "ordinary merge producer response unavailable"
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,410 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "time"
4
- require "uri"
5
- require "ace/git/atoms/delivery_parameters"
6
- require_relative "../atoms/evidence_digest"
7
- require_relative "../atoms/fixed_merge_schemas"
8
-
9
- module Ace
10
- module Assign
11
- module Molecules
12
- # Closed wire contract for the ordinary fixed merge producer
13
- # (`8x3.t.xz9.4`) consumed by the qkb.1 ordinary merge consumer through
14
- # `ace-lab service request/status --mode ordinary-merge`.
15
- #
16
- # The consumer invokes and validates the maintained envelopes without
17
- # translation or flattening. Every validator rejects unknown fields at
18
- # every nesting level and enforces the published patterns, formats and
19
- # semantic joins. The terminal receipt stays embedded: request
20
- # selectors live only under the single `request` object, and succeeded
21
- # (merged/already_merged) versus proven-no-effect failed are mutually
22
- # exclusive.
23
- module OrdinaryMergeContract
24
- REQUEST_SCHEMA = "ace.lab.service.ordinary-merge-request/v1"
25
- ACK_SCHEMA = "ace.lab.service.ordinary-merge-ack/v1"
26
- REFUSAL_SCHEMA = "ace.lab.service.ordinary-merge-refusal/v1"
27
- STATUS_SCHEMA = "ace.lab.service.ordinary-merge-status/v1"
28
- STATUS_ERROR_SCHEMA = "ace.lab.service.ordinary-merge-status-error/v1"
29
- RECEIPT_SCHEMA = "ace.assign.fixed-effect-receipt/v1"
30
- REVIEW_SCHEMA = "ace.review.acceptance-reference/v1"
31
- TEST_SCHEMA = "ace.test.execution-reference/v1"
32
-
33
- REFUSAL_CODES = %w[
34
- ordinary_merge_invalid_request
35
- ordinary_merge_unauthenticated
36
- ordinary_merge_forbidden
37
- ordinary_merge_selection_mismatch
38
- ordinary_merge_authorization_invalid
39
- ordinary_merge_provenance_mismatch
40
- ordinary_merge_target_changed
41
- ordinary_merge_evidence_invalid
42
- ordinary_merge_request_conflict
43
- ordinary_merge_unavailable
44
- ].freeze
45
-
46
- # A caller-side blocker: never a proven refusal, never authorization
47
- # for resubmission. Every other code is an authenticated receiver
48
- # refusal that proves no effect began.
49
- UNAVAILABLE_CODE = "ordinary_merge_unavailable"
50
- PROVEN_NO_EFFECT_CODES = (REFUSAL_CODES - [UNAVAILABLE_CODE]).freeze
51
-
52
- STATES = %w[pending uncertain succeeded failed].freeze
53
- TERMINAL_STATES = %w[succeeded failed].freeze
54
- OUTCOMES = %w[merged already_merged failed_no_effect].freeze
55
-
56
- HEX64 = /\A[0-9a-f]{64}\z/
57
- SHA40 = /\A[0-9a-f]{40}\z/
58
- FORGE_NAME = /\A[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\z/
59
- DATETIME = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/
60
-
61
- REQUEST_KEYS = %w[schema forge pr_url head_repository head_ref candidate_head
62
- base_repository base_ref expected_base_head method review_evidence test_evidence].freeze
63
- REVIEW_KEYS = %w[schema reference reviewer verdict reviewed_head recorded_at record_sha256].freeze
64
- TEST_KEYS = %w[schema reference candidate_head command result recorded_at receipt_sha256].freeze
65
- ACK_KEYS = %w[schema type request_id input_digest reference].freeze
66
- REFUSAL_KEYS = %w[schema code message request_id input_digest].freeze
67
- STATUS_KEYS = %w[schema state reference request_id input_digest receipt].freeze
68
- RECEIPT_KEYS = %w[schema reference request_id input_digest terminal_status request
69
- producer effect remote_observation].freeze
70
- RECEIPT_REQUEST_KEYS = %w[project assignment attempt queue_entry operation authorization input].freeze
71
- PRODUCER_KEYS = %w[receiver_id principal configuration_sha256].freeze
72
- EFFECT_KEYS = %w[started_at finished_at outcome failure_code no_effect_evidence].freeze
73
- OBSERVATION_KEYS = %w[forge_instance pr_identity_url target_repository_identity target_ref_observed
74
- merged_candidate_head admission_target_head resulting_target_head merge_commit
75
- checkout_id worktree_id branch].freeze
76
-
77
- class << self
78
- # Validates and returns the closed ordinary-merge request input.
79
- # Both packages enforce `format` and reject unknown fields at every
80
- # nesting level; evidence heads must equal the candidate head.
81
- def validate_request_input!(document)
82
- exact_object!(document, REQUEST_KEYS, "request input")
83
- const!(document.fetch("schema"), REQUEST_SCHEMA, "request input schema")
84
- pattern!(document.fetch("forge"), FORGE_NAME, "forge")
85
- url!(document.fetch("pr_url"))
86
- bounded!(document.fetch("head_repository"), 1024, "head_repository")
87
- bounded!(document.fetch("head_ref"), 255, "head_ref")
88
- pattern!(document.fetch("candidate_head"), SHA40, "candidate_head")
89
- bounded!(document.fetch("base_repository"), 1024, "base_repository")
90
- bounded!(document.fetch("base_ref"), 255, "base_ref")
91
- pattern!(document.fetch("expected_base_head"), SHA40, "expected_base_head")
92
- const!(document.fetch("method"), "merge", "method")
93
- review = document.fetch("review_evidence")
94
- exact_object!(review, REVIEW_KEYS, "review_evidence")
95
- const!(review.fetch("schema"), REVIEW_SCHEMA, "review_evidence schema")
96
- bounded!(review.fetch("reference"), 256, "review_evidence.reference")
97
- bounded!(review.fetch("reviewer"), 128, "review_evidence.reviewer")
98
- const!(review.fetch("verdict"), "approved", "review_evidence.verdict")
99
- pattern!(review.fetch("reviewed_head"), SHA40, "review_evidence.reviewed_head")
100
- datetime!(review.fetch("recorded_at"), "review_evidence.recorded_at")
101
- pattern!(review.fetch("record_sha256"), HEX64, "review_evidence.record_sha256")
102
- tests = document.fetch("test_evidence")
103
- raise ArgumentError, "request input test_evidence must be a non-empty array" unless tests.is_a?(Array) && !tests.empty?
104
- seen = []
105
- tests.each_with_index do |item, index|
106
- exact_object!(item, TEST_KEYS, "test_evidence[#{index}]")
107
- const!(item.fetch("schema"), TEST_SCHEMA, "test_evidence[#{index}] schema")
108
- bounded!(item.fetch("reference"), 256, "test_evidence[#{index}].reference")
109
- pattern!(item.fetch("candidate_head"), SHA40, "test_evidence[#{index}].candidate_head")
110
- bounded!(item.fetch("command"), 2048, "test_evidence[#{index}].command")
111
- const!(item.fetch("result"), "passed", "test_evidence[#{index}].result")
112
- datetime!(item.fetch("recorded_at"), "test_evidence[#{index}].recorded_at")
113
- pattern!(item.fetch("receipt_sha256"), HEX64, "test_evidence[#{index}].receipt_sha256")
114
- entry = Atoms::EvidenceDigest.digest(item)
115
- raise ArgumentError, "request input test_evidence entries must be unique" if seen.include?(entry)
116
- seen << entry
117
- end
118
- unless review.fetch("reviewed_head") == document.fetch("candidate_head") &&
119
- tests.all? { |item| item.fetch("candidate_head") == document.fetch("candidate_head") }
120
- raise ArgumentError, "request input evidence heads must equal the candidate head"
121
- end
122
- document
123
- end
124
-
125
- # Canonical lowercase SHA-256 input digest of a validated document.
126
- # Canonical request digest — spec ruling r4: the shared producer/
127
- # consumer implementation lives in Atoms::FixedMergeSchemas
128
- # (canonical_request_digest); this contract deliberately keeps no
129
- # local digest computation so both packages can never diverge.
130
-
131
- # Admission acknowledgment: exact keys, exact selector echo, opaque
132
- # reference with no parseable structure implied.
133
- def validate_ack!(data, request_id:, input_digest:)
134
- exact_object!(data, ACK_KEYS, "acknowledgment")
135
- const!(data.fetch("schema"), ACK_SCHEMA, "acknowledgment schema")
136
- const!(data.fetch("type"), "accepted", "acknowledgment type")
137
- selector!(data.fetch("request_id"), "acknowledgment request_id")
138
- pattern!(data.fetch("input_digest"), HEX64, "acknowledgment input_digest")
139
- selector!(data.fetch("reference"), "acknowledgment reference")
140
- unless data.fetch("request_id") == request_id && data.fetch("input_digest") == input_digest
141
- raise ArgumentError, "acknowledgment selectors differ from the recorded binding"
142
- end
143
- data
144
- end
145
-
146
- # Closed outer response envelope: status "ok" carries exactly the
147
- # data object; status "error" carries exactly the error object. The
148
- # outer objects reject unknown fields; the returned branch payload
149
- # is validated by the inner contract validators. Any other outer
150
- # status or shape is malformed. Returns {status:, payload:}.
151
- def validate_envelope_branch!(envelope)
152
- unless envelope.is_a?(Hash) && %w[ok error].include?(envelope["status"])
153
- raise ArgumentError, "response envelope status must be ok or error"
154
- end
155
- if envelope["status"] == "ok"
156
- exact_object!(envelope, %w[status data], "response envelope")
157
- payload = envelope.fetch("data")
158
- else
159
- exact_object!(envelope, %w[status error], "response envelope")
160
- payload = envelope.fetch("error")
161
- end
162
- raise ArgumentError, "response envelope branch must be a JSON object" unless payload.is_a?(Hash)
163
- {status: envelope["status"], payload: payload}
164
- end
165
-
166
- # Pre-admission refusal envelope. Refusal is request-time only; it
167
- # never carries a reference and is never a read-only status value.
168
- def validate_refusal!(error, schema: REFUSAL_SCHEMA)
169
- exact_object!(error, REFUSAL_KEYS, "refusal")
170
- const!(error.fetch("schema"), schema, "refusal schema")
171
- unless REFUSAL_CODES.include?(error.fetch("code"))
172
- raise ArgumentError, "refusal code is outside the closed set"
173
- end
174
- message = error.fetch("message")
175
- unless message.is_a?(String) && !message.empty? && message.length <= 512
176
- raise ArgumentError, "refusal message must be a sanitized non-empty string of at most 512 characters"
177
- end
178
- selector!(error.fetch("request_id"), "refusal request_id")
179
- digest = error.fetch("input_digest")
180
- valid_digest = digest.nil? || (digest.is_a?(String) && digest.match?(HEX64))
181
- unless valid_digest
182
- raise ArgumentError, "refusal input_digest must be a lowercase SHA-256 string or null"
183
- end
184
- error
185
- end
186
-
187
- # Read-only status success envelope. Reports only pending,
188
- # uncertain, succeeded or failed; succeeded/failed carry the one
189
- # embedded terminal receipt, pending/uncertain a null receipt.
190
- def validate_status!(envelope, reference:, request_id:, input_digest:)
191
- exact_object!(envelope, %w[status data], "status envelope")
192
- const!(envelope.fetch("status"), "ok", "status envelope")
193
- data = envelope.fetch("data")
194
- exact_object!(data, STATUS_KEYS, "status data")
195
- const!(data.fetch("schema"), STATUS_SCHEMA, "status schema")
196
- unless STATES.include?(data.fetch("state"))
197
- raise ArgumentError, "status state is outside the closed model"
198
- end
199
- selector!(data.fetch("reference"), "status reference")
200
- selector!(data.fetch("request_id"), "status request_id")
201
- pattern!(data.fetch("input_digest"), HEX64, "status input_digest")
202
- unless data.fetch("reference") == reference && data.fetch("request_id") == request_id &&
203
- data.fetch("input_digest") == input_digest
204
- raise ArgumentError, "status identifiers differ from the recorded binding"
205
- end
206
- receipt = data.fetch("receipt")
207
- if TERMINAL_STATES.include?(data.fetch("state"))
208
- raise ArgumentError, "terminal status requires the embedded receipt" unless receipt.is_a?(Hash)
209
- validated = validate_receipt!(receipt)
210
- raise ArgumentError, "receipt terminal_status differs from the status state" unless validated.fetch("terminal_status") == data.fetch("state")
211
- else
212
- raise ArgumentError, "pending/uncertain status must carry a null receipt" unless receipt.nil?
213
- validated = nil
214
- end
215
- {state: data.fetch("state"), receipt: validated}
216
- end
217
-
218
- # Terminal receipt: embedded request, never flattened. When
219
- # `binding` is given, every command selector and the complete input
220
- # document must equal the recorded binding; success additionally
221
- # joins the remote observation back to the embedded request.
222
- def validate_receipt!(receipt, binding: nil)
223
- exact_object!(receipt, RECEIPT_KEYS, "terminal receipt")
224
- const!(receipt.fetch("schema"), RECEIPT_SCHEMA, "receipt schema")
225
- selector!(receipt.fetch("reference"), "receipt reference")
226
- selector!(receipt.fetch("request_id"), "receipt request_id")
227
- pattern!(receipt.fetch("input_digest"), HEX64, "receipt input_digest")
228
- unless %w[succeeded failed].include?(receipt.fetch("terminal_status"))
229
- raise ArgumentError, "receipt terminal_status is outside the closed set"
230
- end
231
- request = receipt.fetch("request")
232
- exact_object!(request, RECEIPT_REQUEST_KEYS, "receipt request")
233
- bounded!(request.fetch("project"), 128, "receipt request.project")
234
- bounded!(request.fetch("assignment"), 128, "receipt request.assignment")
235
- bounded!(request.fetch("attempt"), 128, "receipt request.attempt")
236
- bounded!(request.fetch("queue_entry"), 128, "receipt request.queue_entry")
237
- const!(request.fetch("operation"), "merge", "receipt request.operation")
238
- selector!(request.fetch("authorization"), "receipt request.authorization")
239
- input = validate_request_input!(request.fetch("input"))
240
- producer = receipt.fetch("producer")
241
- exact_object!(producer, PRODUCER_KEYS, "receipt producer")
242
- bounded!(producer.fetch("receiver_id"), 128, "receipt producer.receiver_id")
243
- bounded!(producer.fetch("principal"), 256, "receipt producer.principal")
244
- pattern!(producer.fetch("configuration_sha256"), HEX64, "receipt producer.configuration_sha256")
245
- unless Atoms::FixedMergeSchemas.canonical_request_digest(
246
- project: request.fetch("project"), assignment: request.fetch("assignment"),
247
- attempt: request.fetch("attempt"), queue_entry: request.fetch("queue_entry"),
248
- authorization: request.fetch("authorization"), request_id: receipt.fetch("request_id"),
249
- input: input
250
- ) == receipt.fetch("input_digest")
251
- raise ArgumentError, "receipt input_digest is not the canonical digest of the embedded binding"
252
- end
253
- validate_effect!(receipt.fetch("effect"))
254
- validate_observation!(receipt.fetch("remote_observation"), input)
255
- enforce_exclusivity!(receipt)
256
- join_observation!(receipt, input)
257
- join_binding!(receipt, binding, input) if binding
258
- receipt
259
- end
260
-
261
- private
262
-
263
- def validate_effect!(effect)
264
- exact_object!(effect, EFFECT_KEYS, "receipt effect")
265
- started_at = effect.fetch("started_at")
266
- datetime!(started_at, "receipt effect.started_at") unless started_at.nil?
267
- datetime!(effect.fetch("finished_at"), "receipt effect.finished_at")
268
- unless OUTCOMES.include?(effect.fetch("outcome"))
269
- raise ArgumentError, "receipt effect.outcome is outside the closed set"
270
- end
271
- failure_code = effect.fetch("failure_code")
272
- unless failure_code.nil? || failure_code == "merge_failed_no_effect"
273
- raise ArgumentError, "receipt effect.failure_code must be null or merge_failed_no_effect"
274
- end
275
- evidence = effect.fetch("no_effect_evidence")
276
- valid_evidence = evidence.nil? || (evidence.is_a?(Hash) &&
277
- evidence.keys.sort == %w[reference sha256].sort &&
278
- bounded?(evidence["reference"], 256) &&
279
- evidence["sha256"].is_a?(String) && evidence["sha256"].match?(HEX64))
280
- unless valid_evidence
281
- raise ArgumentError, "receipt effect.no_effect_evidence must be null or a bounded reference/sha256 object"
282
- end
283
- end
284
-
285
- def validate_observation!(observation, input)
286
- return if observation.nil?
287
- exact_object!(observation, OBSERVATION_KEYS, "receipt remote_observation")
288
- pattern!(observation.fetch("forge_instance"), FORGE_NAME, "remote_observation.forge_instance")
289
- url!(observation.fetch("pr_identity_url"), "remote_observation.pr_identity_url")
290
- bounded!(observation.fetch("target_repository_identity"), 1024, "remote_observation.target_repository_identity")
291
- bounded!(observation.fetch("target_ref_observed"), 255, "remote_observation.target_ref_observed")
292
- %w[merged_candidate_head admission_target_head resulting_target_head merge_commit].each do |key|
293
- pattern!(observation.fetch(key), SHA40, "remote_observation.#{key}")
294
- end
295
- bounded!(observation.fetch("checkout_id"), 256, "remote_observation.checkout_id")
296
- bounded!(observation.fetch("worktree_id"), 256, "remote_observation.worktree_id")
297
- bounded!(observation.fetch("branch"), 255, "remote_observation.branch")
298
- end
299
-
300
- # Succeeded (merged/already_merged) versus proven-no-effect failed
301
- # are mutually exclusive; failure is terminal only with immutable
302
- # no-effect evidence.
303
- def enforce_exclusivity!(receipt)
304
- effect = receipt.fetch("effect")
305
- case receipt.fetch("terminal_status")
306
- when "succeeded"
307
- unless %w[merged already_merged].include?(effect.fetch("outcome")) &&
308
- effect.fetch("failure_code").nil? && effect.fetch("no_effect_evidence").nil? &&
309
- effect.fetch("started_at").is_a?(String) && receipt.fetch("remote_observation").is_a?(Hash)
310
- raise ArgumentError, "succeeded receipt must describe a verified remote outcome without failure evidence"
311
- end
312
- when "failed"
313
- unless effect.fetch("outcome") == "failed_no_effect" &&
314
- effect.fetch("failure_code") == "merge_failed_no_effect" &&
315
- effect.fetch("no_effect_evidence").is_a?(Hash) && receipt.fetch("remote_observation").nil?
316
- raise ArgumentError, "failed receipt must prove no effect and carry no remote observation"
317
- end
318
- end
319
- end
320
-
321
- # `remote_observation` uses distinct observation keys whose values
322
- # are joined back to the embedded request; the fixed merge method
323
- # additionally requires merge_commit == resulting_target_head.
324
- def join_observation!(receipt, input)
325
- observation = receipt.fetch("remote_observation")
326
- return if observation.nil?
327
- unless observation.fetch("forge_instance") == input.fetch("forge") &&
328
- observation.fetch("pr_identity_url") == input.fetch("pr_url") &&
329
- observation.fetch("target_repository_identity") == input.fetch("base_repository") &&
330
- observation.fetch("target_ref_observed") == input.fetch("base_ref") &&
331
- observation.fetch("merged_candidate_head") == input.fetch("candidate_head") &&
332
- observation.fetch("admission_target_head") == input.fetch("expected_base_head") &&
333
- observation.fetch("merge_commit") == observation.fetch("resulting_target_head")
334
- raise ArgumentError, "remote observation does not join back to the embedded request"
335
- end
336
- end
337
-
338
- def join_binding!(receipt, binding, input)
339
- request = receipt.fetch("request")
340
- expected = binding.fetch(:request)
341
- unless receipt.fetch("reference") == binding.fetch(:reference) &&
342
- receipt.fetch("request_id") == binding.fetch(:request_id) &&
343
- receipt.fetch("input_digest") == binding.fetch(:input_digest) &&
344
- request.fetch("project") == expected.fetch(:project) &&
345
- request.fetch("assignment") == expected.fetch(:assignment) &&
346
- request.fetch("attempt") == expected.fetch(:attempt) &&
347
- request.fetch("queue_entry") == expected.fetch(:queue_entry) &&
348
- request.fetch("authorization") == expected.fetch(:authorization) &&
349
- input == expected.fetch(:input)
350
- raise ArgumentError, "embedded request selectors differ from the recorded binding"
351
- end
352
- end
353
-
354
- def exact_object!(value, keys, label)
355
- raise ArgumentError, "#{label} must be a JSON object" unless value.is_a?(Hash)
356
- unless value.keys.sort == keys.sort
357
- raise ArgumentError, "#{label} must have exactly the closed field set"
358
- end
359
- end
360
-
361
- def const!(value, expected, label)
362
- return value if value == expected
363
- raise ArgumentError, "#{label} must be #{expected.inspect}"
364
- end
365
-
366
- def pattern!(value, regex, label)
367
- return value if value.is_a?(String) && value.match?(regex)
368
- raise ArgumentError, "#{label} does not match the closed pattern"
369
- end
370
-
371
- def bounded?(value, max)
372
- value.is_a?(String) && !value.empty? && value.length <= max
373
- end
374
-
375
- def bounded!(value, max, label)
376
- return value if bounded?(value, max)
377
- raise ArgumentError, "#{label} must be a non-empty string of at most #{max} characters"
378
- end
379
-
380
- def selector!(value, label)
381
- bounded!(value, 256, label)
382
- end
383
-
384
- def datetime!(value, label)
385
- unless value.is_a?(String) && value.match?(DATETIME) &&
386
- Time.iso8601(value)
387
- raise ArgumentError, "#{label} must be an RFC 3339 date-time"
388
- end
389
- rescue ArgumentError
390
- raise ArgumentError, "#{label} must be an RFC 3339 date-time"
391
- end
392
-
393
- # `format: uri` plus the maintained neutral PR URL validation: the
394
- # normalized URL produced by the selected forge adapter.
395
- def url!(value, label = "pr_url")
396
- unless value.is_a?(String) && !value.empty? && value.length <= 2048 &&
397
- !value.match?(/\s/) && URI.parse(value).absolute? &&
398
- %w[http https].include?(URI.parse(value).scheme)
399
- raise ArgumentError, "#{label} must be an absolute http(s) URI"
400
- end
401
- Ace::Git::Atoms::DeliveryParameters.validate_url!(value)
402
- value
403
- rescue URI::InvalidURIError, Ace::Git::Error
404
- raise ArgumentError, "#{label} must be a normalized forge PR URL"
405
- end
406
- end
407
- end
408
- end
409
- end
410
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/herdr/molecules/guarded_native_origin"
4
- require_relative "../models/evidence_event"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Exact accepted launch record; this reader grants no native effect.
10
- module OriginalLaunchBinding
11
- def self.verify!(events:, state:, params:)
12
- unless state.is_a?(Hash) && state["guarded_origin"].is_a?(Hash) && state["process_binding"].is_a?(Hash)
13
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is unavailable"
14
- end
15
- unless Models::EvidenceEvent.chain_valid?(events)
16
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record chain is corrupt"
17
- end
18
- records = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "record_launch" }
19
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is ambiguous or absent" unless records.size == 1
20
- record = records.first
21
- data = record.fetch("payload").fetch("data")
22
- unless data.is_a?(Hash) && record["attempt_id"] == params.fetch("attempt_id") &&
23
- %w[mapping_id assignment_id attempt_id].all? { |key| data[key] == params.fetch(key) } &&
24
- data["process_binding"] == state.fetch("process_binding") && data["guarded_origin"] == state.fetch("guarded_origin")
25
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record differs from accepted launch"
26
- end
27
- binding = data.fetch("process_binding")
28
- origin = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(data.fetch("guarded_origin"),
29
- terminal_id: binding.fetch("terminal_id"), child: binding.fetch("process_identity"))
30
- {"binding_digest" => record.fetch("digest").dup.freeze, "origin" => origin}.freeze
31
- rescue KeyError, TypeError, Ace::Runtime::RuntimeUnavailableError
32
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is malformed"
33
- end
34
- end
35
- end
36
- end
37
- end